Программирование PC Rust: общая концепция программирования Sun, September 08 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.

Rust: общая концепция программирования Печать
Добавил(а) microsin   

Это перевод главы 3 книги "The Rust Programming Language" [1]. Здесь раскрываются концепции, которые имеются почти в каждом языке программирования, и как он работают в Rust. Разные языки программирования в своей основе содержат много общего. Ни одна из концепций, представленных в этой главе, не является уникальной для Rust, но мы обсудим их в контексте Rust и объясним соглашения вокруг использования этих концепций. Будут рассмотрены вопросы переменных, базовых типов, функций, комментарии, управление потоком вычислений.

Ключевые слова. В языке Rust существует набор ключевых слов, зарезервированных в использовании только в контексте языка, точно как в других языках программирования. Имейте в виду, что эти слова нельзя использовать как имена для переменных и функций. Большинство ключевых слов обладают особым смыслом, и вы будете использовать их для различных задач в своих программах на Rust. У некоторых ключевых слов пока нет связанной с ними функциональности, но они зарезервированы в Rust для добавления такой функциональности в будущем. Вы можете найти список ключевых слов в Appendix A.

[Переменные и их изменчивость]

Как упоминалось в секции "Сохранение значений в переменных" главы 2 [2], где мы на практике рассматривали программирование игры, на языке Rust переменные по умолчанию не мутируемые (immutable), т. е. ведут себя как константы, которые нельзя поменять во время выполнения программы. Это один из многих стимулов Rust, которым он предлагает писать код таким образом, чтобы воспользоваться преимуществами безопасности и простой реализации параллелизма, предлагаемыми Rust. Конечно, у вас всегда есть возможность сделать переменную изменяемой (mutable). Давайте рассмотрим, почему Rust призывает вас придерживаться immutability, и почему вы иногда можете от этого отказаться.

Когда переменная immutable, её значение может привязано к ней однократно, после чего во время выполнения программы изменение переменной запрещено. Чтобы показать это, давайте сгенерируем новый проект с именем variables в вашей директории проектов projects командой cargo new variables.

$ cargo new variables
$ cd variables

После этого откройте файл src/main.rs и замените его код на следующий:

fn main() {
    let x = 5;
    println!("Значение x равно: {x}");
    x = 6;
    println!("Значение x равно: {x}");
}

Сохраните файл кода и запустите его командой cargo run. Вы должны получить сообщение об ошибке, относящейся к immutability переменной x, как показано в следующем выводе:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:4:5
  |
2 |     let x = 5;
  |         -
  |         |
  |         first assignment to `x`
  |         help: consider making this binding mutable: `mut x`
3 |     println!("Значение x равно: {x}");
4 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable

For more information about this error, try `rustc --explain E0384`. error: could not compile `variables` (bin "variables") due to 1 previous error

Этот пример показывает, как компилятор помогает вам находить ошибки в программе. В этом примере ошибка "cannot assign twice to immutable variable `x`" связана в тем, что была сделана попытка изменить immutable переменную x присвоением ей другого значения.

Получить ошибку во время компиляции важнее, чем столкнуться с проблемами во время выполнения программы, когда мы пытаемся изменить значение, которое изначально предполагалось как не изменяемое, потому что такая ситуация потенциально может привести к багам. Если одна часть кода работает в предположении, что значение переменной никогда не меняется, а другая часть кода меняет это значение, то вполне возможно, что вторая часть кода не будет делать то, для чего была разработана. Причину такого бага трудно отследить постфактум, особенно когда вторая часть программы меняет значение лишь иногда. Компилятор Rust гарантирует, что когда вы заявляете состояние не изменится, то оно действительно не изменится, поэтому не требуется это отслеживать самостоятельно. Ваш код таким образом становится легче понять.

Однако изменчивость переменной (mutability) может быть необходимой. Хотя переменные по умолчанию не изменяемые (immutable), вы можете сделать их изменяемыми путем добавления перед их именем ключевого слова mut, как мы это уже делали в главе 2 [2]. Добавление mut также передает намерение другим читателям кода, указывая тем самым, что другие части кода будут менять значение этой переменной.

Например, давайте поменяем src/main.rs следующим образом:

fn main() {
    let mut x = 5;
    println!("Значение x равно: {x}");
    x = 6;
    println!("Значение x равно: {x}");
}

Если запустить программу снова, то получим следующее:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/variables`
Значение x равно: 5
Значение x равно: 6

Добавлением mut мы разрешили изменять значение x. Использовать или нет изменчивость для переменной в конечном счете зависит от вас, насколько ясно вы представляете себе ситуацию по использованию этой переменной в программе.

Константы. Как и у immutable переменных, у констант имеется определенное заданное изначально значение, привязанное к имени, которое нельзя поменять во время выполнения программы. Однако есть некоторые отличия между константами и переменными.

Во-первых, с константами нельзя использовать ключевое слово mut. Константы не просто immutable по умолчанию - они immutable всегда. Константы декларируются с использованием ключевого слова const вместо ключевого слова let, и при этом должен быть обозначен тип значения. Мы рассмотрим типы и обозначение типа в следующей секции "Типы данных", так что пока не беспокойтесь об этих подробностях. Просто запомните, что для констант указание типа необходимо всегда.

Константы могут декларироваться в любой области видимости, включая глобальную, что делает их полезными для значений, которые должны знать многие части кода.

Последнее отличие констант от переменных состоит в том, что константу может быть установлено только константное выражение, которое может быть вычислено в момент компиляции, но не результат вычисления, который может быть получен во время выполнения программы.

Вот пример декларации константы:

const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

Имя константы THREE_HOURS_IN_SECONDS и её значение устанавливается как результат умножения 60 (количество секунд в минуте) на 60 (количество минут в часе) и на 3 (количество часов, которое мы задаем в своей программе). Соглашение именования в Rust предполагает использование заглавных букв с символами подчеркивания между словами. Компилятор сможет вычислить ограниченное количество операций во время компиляции кода, что позволяет нам задавать необходимое значение в понятном для чтения виде, вместо того, чтобы устанавливать эту константу значением 10800. Для дополнительной информации о том, какие операции могут использоваться для вычисления констант Rust при их декларации, см. документацию [3].

Значения констант достоверны в течение всего времени работы программы, во всей области видимости, где константа задекларирована. Это свойство констант делает их полезными для значений в домене приложения, когда о них должны знать несколько частей программы. Это может быть, например, максимальное количество очков, которое может получить каждый игрок, или скорость света.

Именование жестко закодированных значений, используемых в вашей программе, таких как константы, полезно в контексте сопровождения кода другими специалистами в будущем. Также полезно обозначить только одно место в вашем коде, где можно поменять постоянное значение во время компиляции, чтобы оно сразу поменялось во многих местах, если это будет необходимо в будущем.

Shadowing. Как мы уже видели на примере программирования игры в главе 2 [2], в Rust есть возможность декларировать новую переменную с тем же именем, какое уже было у предыдущей переменной. В этом случае говорят, что первая переменная "затеняется" (shadowed) второй переменной. Это означает, что вторая переменная это то, что компилятор увидит, когда вы используете эту переменную. По сути вторая переменная затмевает первую, принимая на себя новый вариант использования того же самого имени до тех пор, пока не произойдет новое затенение, либо пока не закончится область действия переменной. Мы можем затенять переменную повторением одного и того же имени переменной с ключевым словом let следующим образом:

fn main() {
    let x = 5;

let x = x + 1;

{ let x = x * 2; println!("Значение x в локальной области видимости: {x}"); }

println!("Значение x равно: {x}"); }

В этой программе сначала к переменной x привязывается значение 5. Затем создается новая переменная x путем повтора let x =, когда берется первоначальное значение переменной и к нему добавляется 1, так что значение x становится равным 6. Затем в отдельной области видимости (в пределах фигурных скобок) третий оператор let также затеняет x и создает новую переменную, умножая предыдущее значение на 2, получая для x значение 12. Когда текущая область действия заканчивается, значение x возвращается к 6. Когда мы запустим программу, то она выведет следующее:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/variables`
Значение x в локальной области видимости: 12
Значение x равно: 6

Затенение отличается от создания изменяемой переменной с помощью mut, потому что мы получим ошибку компиляции, если случайно попробуете переназначить эту переменную без ключевого слова let. При использовании let мы можем проделать несколько преобразований значения, однако переменная будет не изменяемая после того, как эти трансформации произошли.

Другое отличие между mut и shadowing состоит в том, что мы с затенением мы по сути создаем совершенно другую, новую переменную, и при этом может поменяться тип переменной, а имя останется таким же. То есть будем использовать старое имя переменной, но уже в новом качестве. Для примера предположим, что программа запрашивает у пользователя, сколько пробелов должно быть между частями текста. Пользователь вводит эти пробелы как текст, и потом вы хотите сохранить количество этих пробелов как числовое значение:

    let spaces = "   ";
    let spaces = spaces.len();

У первой переменной spaces строковый тип, а у второй переменной spaces тип целочисленный. Такое затенение предохраняет нас от необходимости задавать для переменных разных типов разные имена, таких как spaces_str и spaces_num; вместо этого мы повторно используем более простое имя spaces. Однако если для этого вы попытаетесь использовать mut, то получите ошибку компиляции:

    let mut spaces = "   ";
    spaces = spaces.len();

Ошибка говорит нам, что не разрешается менять тип изменяемой переменной:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
 --> src/main.rs:3:14
  |
2 |     let mut spaces = "   ";
  |                      ----- expected due to this value
3 |     spaces = spaces.len();
  |              ^^^^^^^^^^^^ expected `&str`, found `usize`
  |
help: try removing the method call
  |
3 -     spaces = spaces.len();
3 +     spaces = spaces;
  |

For more information about this error, try `rustc --explain E0308`. error: could not compile `variables` (bin "variables") due to 1 previous error

Мы рассмотрели работу переменных, теперь давайте обсудим типы, какие могут быть назначены для переменных.

[Типы данных]

Каждое значение в Rust это определенный тип данных, который говорит для Rust, какие определены данные, чтобы знать, как работать с этими данными. Мы рассмотрим два подмножества типов данных: скалярный (scalar) и составной (compound).

Имейте в виду, что Rust является статически типизированным языком. Это означает, что он должен знать типы всех переменных во время компиляции. Компилятор обычно может определить, какой тип мы хотим использовать, на основе его значения, и на основе того, как мы его используем. В случаях, когда возможны многие типы, например когда мы преобразовали String в числовой тип с помощью parse в секции "Сравнение угадываемого числа с секретным" главы 2, мы должны добавить аннотацию типа, примерно так:

let guess: u32 = "42".parse().expect("Это не число!");

Аннотация здесь это ": u32". Если мы не добавим аннотацию типа ": u32", то Rust отобразит следующую ошибку, означающую, что компилятору нужно больше информации о том, какой тип мы хотим использовать:

$ cargo build
   Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0284]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let guess = "42".parse().expect("Not a number!");
  |         ^^^^^        ----- type must be known at this point
  |
  = note: cannot satisfy `< _ as FromStr>::Err == _`
help: consider giving `guess` an explicit type
  |
2 |     let guess: /* Type */ = "42".parse().expect("Not a number!");
  |              ++++++++++++

For more information about this error, try `rustc --explain E0284`. error: could not compile `no_type_annotations` (bin "no_type_annotations") due to 1 previous error

Вы увидите разные типы аннотаций для других типов данных.

Скалярные типы. Скалярный тип представляет одно значение. В Rust есть 4 основных скалярных типа: целые числа, числа с плавающей запятой, Boolean и символы. Вы можете распознать аналогичные типы в других языках программирования. Давайте перейдем к тому, как эти типы работают Rust.

Целочисленные типы. Целое число это такое число, у которого нет дробной части. В главе 2 мы использовали один такой тип, u32. Эта декларация типа показывает, что связанное с ним целое значение не имеет знака (обозначение типа целого со знаком начинается с i вместо u), и что оно имеет разрядность 32 бита. В таблице 3-1 показаны встроенные целочисленные типы Rust. Мы можем использовать любые из этих вариантов для декларации значения целого числа.

Таблица 3-1. Целочисленные типы в Rust.

Разрядность signed unsigned
8 i8 u8
16 i16 u16
32 i32 u32
64 i64 u64
128 i128 u128
архитектура isize usize

Каждый вариант типа может быть либо со знаком (signed), либо без знака (unsigned), и он имеет явный размер. Числа signed сохраняются в формате двоичного дополнения (two’s complement notation [4]).

Каждое signed значение может сохранять числа от -(2n - 1) до 2n - 1 - 1 включительно, где n это количество бит, используемое для числа. Таким образом, i8 может сохранять числа от -(27) до 27 - 1, что соответствует диапазону чисел от -128 до 127. Unsigned значения могут сохранять числа от 0 до 2n - 1, так что u8 может сохранять числа от 0 до 28 - 1, т. е. от 0 до 255.

Дополнительно типы isize и usize зависят от архитектуры компьютера, на котором запускается ваша программа, что обозначается в таблице как "архитектура": 64 бита, если у вас 64-разрядная архитектура, и 32 бита, если архитектура 32-разрядная.

В коде вы можете записывать целочисленные литералы в любой из форм, показанных в таблице 3-2. Обратите внимание, что числовые литералы, которые могут быть несколькими числовыми типами, позволяют для обозначения типа использовать суффикс типа, например 57u8. Числовые литералы также могут использовать _ как визуальный сепаратор, чтобы упростить чтение числа. Например 1_000 будет иметь такое же значение как если бы вы просто указали 1000.

Таблица 3-2. Целочисленные литералы в Rust.

Числовые литералы Пример
Десятичное значение (decimal) 98_222
Шестнадцатеричное значение (hex) 0xff
Восьмеричное значение (octal) 0o77
Двоичное значение (binary) 0b1111_0000
Байт (только u8) b'A'

Как все-таки определить, какие целочисленные типы использовать? Если вы не уверены в ответе на этот вопрос, то умолчания Rust будут обычно хорошей точкой для старта: по умолчанию применяется тип i32. Основная ситуация, в которой вы будете использовать isize или usize, это индексация какой-либо коллекции.

Предположим, что у вас есть переменная типа u8, которая может хранить значения между 0 и 255. Если вы попытаетесь изменить значение этой переменной вне этого диапазона, например на 256, то произойдет переполнение (overflow), которое в результате может привести к двум вариантам поведения. Когда вы компилируете в режиме отладки (debug), Rust добавляет в код проверки переполнения целых чисел, что при переполнении приведет к ситуации паники во время выполнение программы (panic runtime). Rust использует термин паники (panic), когда программа завершает работу с ошибкой; подробнее ситуации паники будут рассмотрены в секции "Unrecoverable Errors with panic!" главы 9.

Когда вы компилируете проект в режиме релиза (release), используя флаг --release, Rust не добавляет проверки целочисленного выполнения, приводящие к панике. Вместо этого при переполнении  происходит циклический перескок значения по принципу двоичного дополнения (two complement wrapping). Если кратко, то значения свыше максимального оборачиваются в сторону минимальных значений. В случае u8 значение 256 превратится в 0, значение 257 превратится в 1, и так далее. Программа не упадет в панику, однако значения переменных при переполнении окажутся совсем не такими, как вы ожидаете. Полагаться на поведение overflow wrapping целых чисел считается ошибкой.

Для явной обработки возможности переполнения можно использовать следующие семейства методов, предоставляемые стандартной библиотекой для примитивных числовых типов:

• Обертка во всех режимах операций через методы wrapping_*, таких как wrapping_add.
• Возвращение значения None, если произошло переполнение, с помощью методов checked_*.
• Возвращать значение и bool индикацию, когда произошло переполнение, с помощью методов overflowing_*.
• Насыщение минимальных и максимальных значений с помощью методов saturating_*.

Типы плавающей точки. В Rust также есть 2 примитивных типа для чисел плавающей точки (floating-point), это такие числа, которые представляются с нотацией десятичной точки. Rust типы floating-point это f32 и f64, размер которых 32 и 64 бита соответственно. По умолчанию используется f64, потому что на современных CPU этот тип обрабатывается почти с такой же скоростью, что и f32, однако точность чисел получается выше. Все floating-point типы знаковые (signed).

Вот так используются числа floating-point:

fn main() {
    let x = 2.0;      // f64
    let y: f32 = 3.0; // f32
}

Числа floating-point представляются в памяти по стандарту IEEE-754 [5]. Тип f32 называют числами плавающей точки одинарной точности (single-precision float), а f64 числами плавающей точки двойной точности (double precision float).

Числовые операции. Rust поддерживает базовые математические операции, которые вы можете ожидать, для всех числовых типов: сложение, вычитание, умножение, деление, получение остатка от деления. Целочисленное деление делает обрезку чисел в направлении к ближайшему меньшему значению, если после деления остается дробный остаток. Следующий код показывает каждую числовую операцию в операторе let:

fn main() {
    // Сложение (addition):
    let sum = 5 + 10;

// Вычитание (subtraction): let difference = 95.5 - 4.3;

// Умножение (multiplication): let product = 4 * 30;

// Деление (division): let quotient = 56.7 / 32.2; let truncated = -5 / 3; // результатом будет -1

// Остаток от деления целых чисел (remainder, операция mod): let remainder = 43 % 5; }

Каждое из выражений в этих строках использует математический оператор, и вычисляемое им одиночное значение привязывается к имени переменной. Appendix B содержит список всех операторов, предоставляемых Rust.

Boolean. Как в большинстве языков программирования, на языке Rust двоичный тип (Boolean) имеет 2 возможных значения: true и false. Значения Boolean имеют размер 1 байт. Тип Boolean в Rust обозначают спецификатором bool. Например:

fn main() {
    let t = true;
    let f: bool = false; // применение явной аннотации типа
}

Значения Boolean в основном используются в управлении потоком, таких как проверка условий выражением if. Как работают выражения if будет подробно рассмотрено в секции "Управление потоком вычислений".

Символьный тип. Тип char на языке Rust это примитивный символьный тип. Вот несколько примеров декларации значений char:

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // применение явной аннотации типа
    let russian_letter = 'Я';
}

Обратите внимание, что мы указываем литералы char через одиночные кавычки, что отличается от строковых литералов, где используются двойные кавычки. Тип char на языке Rust занимает 4 байта, и представляет скалярное значение Юникода (Unicode Scalar Value), т. е. оно может представить намного большее количество символов, чем обычный ASCII (в том числе и национальные символы). Символы акцента, китайские, японские символы, эмодзи, русские буквы - все это допустимые значения char в Rust. Unicode Scalar Value находится в диапазоне от U+0000 до U+D7FF и от U+E000 до U+10FFFF включительно. Однако "символ" (character) на самом деле не является концепцией Unicode, и ваше человеческое представление о том, что такое "символ", может не совпадать с тем, что подразумевается под char на языке Rust. Более подробно эта тема обсуждается в секции "Storing UTF-8 Encoded Text with Strings" главы 8.

[Составные типы]

Составные (compound) типы могут группировать несколько значений в один тип. В Rust есть два примитивных составных типа: кортеж (tuple) и массив (array).

Tuple. Кортеж это обычный способ группировать вместе несколько типов в один составной тип. У кортежа размер фиксирован: после декларации он не может уменьшаться или увеличиваться в размере.

Мы создаем кортеж написанием списка значений внутри круглых скобок. Каждая позиция в кортеже имеет тип, и типы различных значений в кортеже не обязательно должны быть одинаковыми. Мы добавили опциональные аннотации типов в следующем примере:

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

Переменная tup привязывается ко всему кортежу, потому что кортеж считается одиночным составным типом. Чтобы получить отдельные значения из кортежа, мы можем использовать шаблон соответствия (pattern matching) для деструктурирования значения шаблона, примерно так:

fn main() {
    let tup = (500, 6.4, 1);

let (x, y, z) = tup;

println!("Значение y: {y}"); }

Эта программа сначала создает кортеж, и привязывает его к переменной tup. Затем используется шаблон с ключевым словом let, чтобы взять tup и превратить его в отдельные переменные x, y и z. Это называется деструктурированием, потому что разбивает одиночный кортеж на три части. И наконец, программа печатает значение y, которое равно 6.4.

Мы также можем получить доступ к одному элементу кортежа, используя точку (.), за которой идет индекс значения, к которому нужен доступ, например:

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);
    let five_hundred = x.0;
    let six_point_four = x.1;
    let one = x.2;
}

Эта программа создает кортеж x, и затем обращается к каждому элементу этого кортежа, используя соответствующий индекс. Как и в большинстве языков программирования, индекс для первого элемента в кортеже равен 0.

Кортеж без каких-либо значений имеет специальное имя unit. Это значение и его соответствующий тип оба записываются как (), и представляют собой пустое значение, или пустой возвращаемый тип. Выражение неявно возвращают значение unit, если они не возвращают никакое другое значение.

Array. Другой способ получить коллекцию нескольких значений это массив (array). В отличие от кортежа (tuple) у всех элементов массива одинаковый тип. В отличие от массивов в некоторых языках программирования, массивы в Rust имеют фиксированную длину.

Мы записываем значения в массиве как список значений, отделенных друг от друга запятой:

fn main() {
    let a = [1, 2, 3, 4, 5];
}

Массивы полезны, когда вы хотите выделить для ваших данных пространство в стеке, вместо того, чтобы использовать для этого кучу (более подробно про стек и кучу будет рассказано в главе 4), или когда вам необходимо обеспечить всегда фиксированное количество элементов. Однако массив не так гибок, как векторный тип. Тип vector это подобная коллекция, предоставляемая стандартной библиотекой, которому разрешено увеличиваться или уменьшаться в размере. Если вы не уверены, использовать массив или вектор, то скорее всего вам нужен вектор. Глава 8 рассматривает vector более подробно.

Однако массивы более полезны, когда вы знаете заранее количество элементов, и вам не нужно изменять это количество. Например, если вы используете в программе имена месяцев, то скорее всего лучше использовать массив вместо вектора, потому что в таком случае в массиве всегда будет 12 элементов:

let months = ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль",
              "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"];

Вы записываете тип массива, используя квадратные скобки, внутри которых указываете тип и затем через точку с запятой количество элементов в массиве, например:

let a: [i32; 5] = [1, 2, 3, 4, 5];

Здесь указано, что массив a хранит 5 значений, и у каждого из них тип i32.

Вы также можете инициализировать массив, в котором будут одинаковые значения каждого элемента, указывая это значение за ним через точку с запятой количество элементов, например:

let a = [3; 5];

В этом примере массив с именем a будет изначально содержать 5 элементов, у каждого элемента будет значение 3. Это дает такой же эффект, как если написать let a = [3, 3, 3, 3, 3];.

Доступ к элементам массива. Массив хранится в одиночном непрерывном куске памяти фиксированного размера, который может быть выделен в стеке. Вы можете получить доступ к элементам массива, используя индекс, примерно так:

fn main() {
    let a = [1, 2, 3, 4, 5];

let first = a[0]; let second = a[1]; }

В этом примере переменная с именем first получит значение 1, потому что значение извлекается из элемента массива с индексом [0]. Переменная second получит из массива значение 2 по индексу [1].

Некорректный доступ к элементам массива. Давайте рассмотрим, что получится, если мы попытаемся получить доступ к ячейке массива за его последним элементом. Если запустить следующий код, то при попытке взять элемент за пределами массива a программа вызовет панику:

use std::io;

fn main() { let a = [1, 2, 3, 4, 5];

println!("Введите индекс в массиве.");

let mut index = String::new();

io::stdin() .read_line(&mut index) .expect("Не получилось прочитать строку");

let index: usize = index .trim() .parse() .expect("Введенный индекс это не число");

let element = a[index];

println!("Значение элемента с индексом {index} равно: {element}"); }

Этот код скомпилируется нормально. Если вы запустите его командой cargo run, и по запросу программы будете вводить индексы 0, 1, 2, 3 или 4, то программа будет печатать соответствующие значения из массива по указанному индексу. Если же вы введете число, которое соответствует индексу за пределами конца массива, такое как 7 или 10, то увидите примерно такой вывод:

thread 'main' panicked at src/main.rs:19:19:
index out of bounds: the len is 5 but the index is 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Программа столкнется с ошибкой времени выполнения (runtime error) в точке, когда было использовано недопустимое значение для операции индексации. Программа аварийно завершит работу с сообщением об ошибке, и не выполнит последний оператор println!. Когда вы обращаетесь к элементу массива по индексу, Rust будет проверять, что индекс должен быть меньше длины массива. Если индекс больше или равен длине массива, Rust генерирует состояние паники. Эта проверка должна выполняться runtime, особенно в этом случае, потому что компилятор не может заранее знать значение, которое введет пользователь.

Этот пример демонстрирует принципы безопасности Rust при работе с памятью. Во многих низкоуровневых языках проверки такого рода не выполняются, и когда вы предоставите некорректный индекс, может произойти недопустимый доступ к памяти. Rust защищает вас от ошибок подобного рода, делая немедленный выход из программы вместо того, чтобы допустить ненормальное обращение к памяти и продолжить работу программы. Глава 9 более подробно обсуждает обработку ошибок (error handling) на языке Rust, и объясняет, как писать удобочитаемый, безопасный код, который не допускает ни паники, ни некорректного обращения к памяти.

[Функции]

Функции преобладают в коде Rust. Вы уже видели одну из самых важных функций языка: функцию main, которая составляет точку входа большинства программ. Также вы видели ключевое слово fn, которое позволяет декларировать новые функции.

Код Rust использует так называемую "змеиную нотацию" (snake case) в качестве обычного написания имен функций и переменных, когда все имена записываются маленькими буквами, а когда имена состоят из нескольких слов, они соединяются друг с другом через символ подчеркивания. Ниже приведен пример программы, содержащий определение функции:

fn main() {
    println!("Hello, world!");

another_function(); }

fn another_function() { println!("Другая функция."); }

Функцию Rust определяют вводом fn, за которым идет имя функции и набор круглых скобок. Фигурные скобки говорят компилятору, где начинается и заканчивается тело функции.

Мы можем вызывать любую функцию, которую определили, путем ввода её имени, за которым идут круглые скобки. Поскольку функция another_function определена в программе, то её можно вызвать из тела функции main. Обратите внимание, что функция another_function определена в исходном коде после функции main; также мы могли бы определить её и перед функцией main. Rust не заботится о том, где вы определили свои функции, только если они определены где-то в области видимости вызывающего кода.

Давайте создадим новый бинарный проект с именем functions чтобы дополнительно рассмотреть функции. Поместите пример показанного выше кода в файл src/main.rs и запустите его. Вы должны увидеть следующий вывод:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/functions`
Hello, world!
Другая функция.

Строки выполняются в том порядке, в каком они следуют друг за другом в теле функции main. Сначала печатается сообщение "Hello, world!", и затем вызывается функция another_function, и печатается её сообщение.

Параметры. Мы можем определить функции, чтобы у них были параметры. Параметры это специальные переменные, которые составляют часть сигнатуры функции. Когда у функции есть параметры, вы можете предоставить для них конкретные значения при вызове функции. Технически эти конкретные значения называют аргументами, но в случайном разговоре часто место путают эти термины, применяя слова "параметр" и "аргумент" взаимозаменяемо как для параметров, так и для конкретных значений, передаваемых в функцию при её вызове.

В этой версии функции another_function мы добавим параметр:

fn main() {
    another_function(5);
}

fn another_function(x: i32) { println!("Значение x равно: {x}"); }

Попробуйте запустить эту программу, вы должны получить следующий вывод:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 1.21s
     Running `target/debug/functions`
Значение x равно: 5

В декларации another_function теперь есть параметр с именем x. Тип x указан как i32. Когда мы передаем 5 в another_function, макрос println! выведет 5 в месте, где пара фигурных скобок, содержащих x в строке формата.

В сигнатурах функции вы обязательно должны декларировать тип каждого параметра. Это обдуманное решение в дизайне Rust: требование аннотаций типа в определениях функции означает, что компилятору почти никогда не нужно, чтобы вы использовали их в другом месте кода чтобы выяснить, какой тип вы имеете в виду. Компилятор также может выдавать более полезные сообщения об ошибках, если он знает, какие ожидать типы функции.

Когда определяется несколько параметров, декларации параметров отделяются друг от друга запятыми, вот так:

fn main() {
    print_labeled_measurement(5, 'h');
}

fn print_labeled_measurement(value: i32, unit_label: char) { println!("Измерено: {value}{unit_label}"); }

В этом примере создана функция с именем print_labeled_measurement с двумя параметрами. Первый параметр носит имя value, и у него тип i32. Второй параметр носит имя unit_label, и у него тип char. Функция печатает текст, содержащий и value, и unit_label.

Попробуйте запустить этот код. Замените текущий код программы в своем файле src/main.rs проекта, и запустите его командой cargo run:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/functions`
Измерено: 5h

Мы передали в функцию значение 5 в качестве value, и 'h' как значение для unit_label, поэтому вывод программы содержит эти значения.

Операторы и выражения. Тела функций состоят из последовательности операторов, которые могут заканчиваться выражением. До сих пор рассмотренные нами функции не включали в себя конечное выражение (ending expression), но вы видели выражение как часть оператора. Поскольку язык Rust является языком, основанным на выражениях, это важное для понимания различие. Другие языки не имеют таких же различий, поэтому давайте рассмотрим, что такое операторы и выражения, и как их различия влияют на тела функций.

• Оператор (statement) это инструкция, которая выполняет какое-то действие в программе, не возвращая при этом значение.
• Выражение (expression) вычисляет значение результата.

Давайте рассмотрим примеры.

Мы уже использовали операторы и выражения. Создадим переменную и назначим её значение с помощью ключевого слова let. Здесь let формирует оператор.

fn main() {
    let y = 6;
}

Листинг 3-1. Декларация функции main, содержащая один оператор.

Определения функций также операторы; весь предыдущий пример это сам по себе оператор.

Операторы не возвращают значения. Таким образом, в не можете назначить оператор let другой переменной, как пытается делать следующий код, он при компиляции даст ошибку:

fn main() {
    let x = (let y = 6);
}

Если вы запустите эту программу, то получите следующее:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found `let` statement
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^
  |
  = note: only supported directly in conditions of `if` and `while` expressions

warning: unnecessary parentheses around assigned value --> src/main.rs:2:13 | 2 | let x = (let y = 6); | ^ ^ | = note: `#[warn(unused_parens)]` on by default help: remove these parentheses | 2 - let x = (let y = 6); 2 + let x = let y = 6; |

warning: `functions` (bin "functions") generated 1 warning error: could not compile `functions` (bin "functions") due to 1 previous error; 1 warning emitted

Оператор let y = 6 не возвращает значение, так что он ничего не может привязать к переменной x. Это поведение отличается от других языков, таких как C и Ruby, где присваивание возвращает значение присваивания. В этих языках вы можете написать x = y = 6, и в результате у обоих переменных x и y окажется значение 6; но это не тот случай в Rust.

Выражения вычисляются до значения, и они составляют большую часть кода, которую вы напишете на Rust. Рассмотрим математическую операцию, такую как 5 + 6, это как раз выражение, которое вычисляется в значение 11. Выражения могут быть частью операторов: в листинге 3-1 число 6 является выражением в операторе let y = 6; это выражение вычисляется в значение 6. Вызов функции это выражение. Вызов макроса это также выражение. Новый блок области видимости (scope block), созданный фигурными скобками, это выражение, например:

fn main() {
    let y = {
        let x = 3;
        x + 1
    };

println!("Значение y равно: {y}"); }

Здесь следующий блок является выражением:

{
    let x = 3;
    x + 1
}

В этом примере блок вычисляется в значение 4. Это значение привязывается к переменной y как оператора let. Обратите внимание, что строка x + 1 без точки с запятой в конец, что отличается от большинства строк кода, которые вы видели до этого. Выражения не содержат в себе завершающих точек с запятой. Если вы добавите точку с запятой в конец выражения, то превратите его в оператор, и тогда не будет возвращаться значение. Имейте это в виду, когда мы рассмотрим далее возвращаемые из функций значения и выражения.

Функции с возвратом значения. Функции могут возвращать значения в код, который их вызвал. Мы не даем имя возвращаемым значениям, но должны обозначить их тип после стрелочки (->). В Rust возвращаемое значение функции это синоним значению последнего выражения в блоке тела функции. Вы можете сделать преждевременный выход из функции, используя ключевое слово return и указав значение, однако большинство функций неявно возвращают значение своего последнего выражения. Вот пример функции five, которая возвращает значение:

fn five() -> i32 {
    5
}

fn main() { let x = five();

println!("Значение x равно: {x}"); }

Здесь в теле функции нет вызовов функций, макросов или даже операторов let. Функция просто сама по себе возвращает число 5. Это по-настоящему допустимая функция в Rust. Обратите внимания, что также указаны возвращаемый тип функции как "-> i32". Попробуйте запустить этот код, вывод должен выглядеть так:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/functions`
Значение x равно: 5

5 в функции five это возвращаемое значение функции, и возвращаемым типом будет i32. Рассмотрим это подробнее. Здесь есть два важных момента: во-первых, строка let x = five(); показывает, что мы используем возвращаемое значение для инициализации переменной. Поскольку функция five возвращает 5, то эта строка делает то же самое, что и строка let x = 5;.

Во-вторых, у функции five нет параметров, и она определяет тип возвращаемого значения, однако тело функции это просто 5 без точки запятой в конце, потому что это выражение, значение которого должно мы хотим возвратить.

Рассмотрим другой пример:

fn main() {
    let x = plus_one(5);

println!("Значение x равно: {x}"); }

fn plus_one(x: i32) -> i32 { x + 1 }

Запуск этого кода выведет "Значение x равно: 6". Однако если мы поместим в конце строки x + 1 точку с запятой, тем самым превратив выражение в оператор, то получим ошибку:

fn main() {
    let x = plus_one(5);

println!("Значение x равно: {x}"); }

fn plus_one(x: i32) -> i32 { x + 1; }

Компиляция этого кода приведет к ошибке:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
 --> src/main.rs:7:24
  |
7 | fn plus_one(x: i32) -> i32 {
  |    --------            ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
8 |     x + 1;
  |          - help: remove this semicolon to return this value

For more information about this error, try `rustc --explain E0308`. error: could not compile `functions` (bin "functions") due to 1 previous error

Основное сообщение об ошибке, не соответствие типа, раскрывает причину проблемы этого кода. Определение функции plus_one говорит, что функция возвращает i32, однако оператор не вычисляется в значение, вместо этого его результат это (), тип unit. Таким образом, ничего из функции не возвращается, что противоречит определению функции, и приводит к ошибке. В этом выводе Rust дает сообщение, которое вероятно поможет разобраться с этой проблемой: он советует удалить точку с запятой, что исправит ошибку.

[Комментарии]

Если вы знакомы с языком C, то дальше можно не читать, потому что комментарии на языке Rust пишутся точно так же, как на языках C и C++.

Все программисты стараются сделать свой код как можно проще для понимания, но иногда для содержимого кода требуется дополнительное пояснение. В таких случаях программисты оставляют в своем исходном коде комментарии, которые компилятор будет игнорировать, но люди, читающие код, могут найти эти комментарии полезными.

Вот простой комментарий:

// hello, world

В Rust стиль идиоматического комментария начинается с двух слешей, и этот комментарий продолжается до конца строки. Комментарии, которые должны продолжаться за пределы одной строки, должны включать // на каждой строке следующим образом:

// Здесь мы сделали что-то очень сложное, так что для описания одной
// строки не хватило! Надеемся, что этот комментарий поможет вам
// понять, что тут происходит.

Комментарии также могут быть размещены в конце строк, содержащих код:

fn main() {
    let lucky_number = 7; // Сегодня я себя чувствую счастливым.
}

Но чаще всего вы увидите следующий вариант комментария, когда он находится над кодом, который комментирует:

fn main() {
    // Сегодня я себя чувствую счастливым.
    let lucky_number = 7;
}

Rust также позволяет другой вид комментария, комментарий документирования ///, который мы обсудим в секции "Publishing a Crate to Crates.io" главы 14.

[Control Flow (управление потоком вычислений)]

Возможность запустить какой-либо код в зависимости от того, истинно (true) условие, или нет (false), и многократно запускать некоторый код, пока условие истинно - все это является основными строительными блоками в большинстве языков программирования. Наиболее распространенными конструкциями, которые позволяют управлять потоком выполнения кода в Rust - выражения if и циклы.

Выражения if. Проверка выражения условия в if позволит вам осуществить ветвление в вашем коде в зависимости от условий. Вы предоставляете условие и затем состояние, "Если условие удовлетворяется, то запустить этот блок кода. Если условие не удовлетворяется, то не запускать этот блок кода".

Создайте новый проект с именем branches в своей директории projects, чтобы рассмотреть выражение if. В файле src/main.rs, введите следующее:

fn main() {
    let number = 3;

if number < 5 { println!("условие true"); } else { println!("условие false"); } }

Все выражения if начинаются с ключевого слова if, за которым идет условие. В нашем примере условие проверяет, верно или нет, что переменная number меньше чем 5. Мы поместили блок кода для выполнения, если условие true, сразу после условия, внутри фигурных скобок. Блоки кода, связанные с условиями if, иногда называют arms, точно так же как arms выражений match, которые мы обсуждали в секции "Сравнение угадываемого числа с секретным" главы 2 [2].

Опционально мы можем также включить выражение else (что мы в этом примере и сделали), чтобы дать программе альтернативный блок кода для выполнения, когда условие if вычислено как false. Если вы не предоставите выражение else, и условие if окажется false, то программа просто пропустит блок if и начнет выполнять следующую часть кода.

Попробуйте запустить этот код, вы должны увидеть следующее:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
Условие true

Попробуйте поменять значение number, чтобы условие стало false, и посмотрите, что произойдет:

    let number = 7;

Запустите программу снова, и увидите вывод:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
Условие false

Также следует заметить, что условие в этом коде должно быть типа bool. Если же условие не bool, то произойдет ошибка. Например, попробуйте запустить следующий код:

fn main() {
    let number = 3;

if number { println!("Значение number было три"); } }

На этот раз условие if вычисляется как 3, и Rust выдаст ошибку:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
 --> src/main.rs:4:8
  |
4 |     if number {
  |        ^^^^^^ expected `bool`, found integer

For more information about this error, try `rustc --explain E0308`. error: could not compile `branches` (bin "branches") due to 1 previous error

Сообщение ошибки показывает, что Rust ожидает bool, но увидел integer. В отличие от языков наподобие Ruby и JavaScript, Rust не будет автоматически пытаться преобразовать не двоичные типы в двоичные. Вы должны явно и всегда предоставлять для if двоичный тип для проверки условия. Например, если мы хотим запустить код блока, только когда number не равен 0, то можно поменять выражение if следующим образом:

fn main() {
    let number = 3;

if number != 0 { println!("number равно значению, отличающемуся от нуля"); } }

Обработка нескольких условий с помощью else if. Вы можете использовать несколько условий, комбинируя if и else в выражении else if. Например:

fn main() {
    let number = 6;

if number % 4 == 0 { println!("number делится на 4"); } else if number % 3 == 0 { println!("число делится на 3"); } else if number % 2 == 0 { println!("число делится на 2"); } else { println!("число не делится на 4, 3 или 2"); } }

В этом примере у программы есть 4 возможных пути для ветвления. После её запуска вы должны увидеть следующий вывод:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
число делится на 3

Когда программа выполняется, она проверяет каждое выражение if по очереди, и выполнить первое тело, у которого условие вычислено как true. Обратите внимание, что хотя 6 также делится и на 2, но мы не увидели вывод соответствующего блока кода "число делится на 2", как не увидели "число не делится на 4, 3 или 2" блока else. Это потому что Rust выполнит только блок первого условия true, и как только оно найдено, остальные условия не проверяются и их блоки не выполняются.

Использование слишком большого количества выражений else if может привести к загромождению кода, поэтому может потребоваться рефакторинг кода. Глава 6 описывает мощную конструкцию ветвления Rust, называемому match, как раз подходящую для таких случаев.

Использование if в операторе let. Поскольку if это выражение, мы можем использовать это в правой части оператора let, чтобы присвоить результат переменной, как показано в листинге 3-2.

fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

println!("Значение number равно: {number}"); }

Листинг 3-2. Назначение переменной результата выражения if.

Переменная number будет привязана к значению, полученному из выражения if. Запустите этот код, чтобы посмотреть, что произойдет:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/branches`
Значение number равно: 5

Помните, что блоки кода оцениваются по последнему в нем выражению, и числа сами по себе также выражения. В этом примере значение выражения if целиком зависит от того, какой блок кода выполнится. Это означает, что выражение каждой ветви if должны иметь одинаковый тип. В листинге 3-2 результаты обоих ветвей кода это целые числа i32. Если типы результатов выражений ветвей не совпадают, то произойдет ошибка, как в следующем примере:

fn main() {
    let condition = true;

let number = if condition { 5 } else { "six" };

println!("Значение number равно: {number}"); }

Когда вы попытаетесь скомпилировать этот код, получится ошибка. Ветви if и else несовместимы друг с другом, а Rust не может на лету во время выполнения кода назначать тип переменным, поэтому он обнаружит проблему в программе:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
 --> src/main.rs:4:44
  |
4 |     let number = if condition { 5 } else { "six" };
  |                                 -          ^^^^^ expected integer, found `&str`
  |                                 |
  |                                 expected because of this

For more information about this error, try `rustc --explain E0308`. error: could not compile `branches` (bin "branches") due to 1 previous error

Выражение блока if вычисляется как целое число, а выражение блока else вычисляется как строка. Это не заработает, потому что переменная может быть только одного типа, и для Rust нужно заранее знать (в момент компиляции) определенно, какой должен быть тип у переменной. Знание типа переменной number позволит компилятору проверить соответствие типа во всех местах, где используется переменная number. Rust не может сделать так, чтобы определять тип переменной runtime, для этого компилятор был бы более сложным и давал меньше гарантий в коде, если бы ему пришлось использовать несколько гипотетических типов для любой переменной.

Циклы. Часто бывает полезно выполнить блок кода несколько раз. Для этой задачи Rust предоставляет несколько видов циклов, которые прокручивают выполнение кода в пределах тела loop, проходя его до конца и сразу перескакивая на выполнение блока от начала. Для экспериментов с циклами давайте создадим новый проект с именем loops.

В Rust есть 2 вида циклов: loop, while и for, мы попробуем каждый из них.

Ключевое слово loop говорит Rust повторять выполнение блока кода снова и снова, бесконечно, пока вы явно не укажете циклу остановиться.

В качестве примера поменяйте файл src/main.rs в вашей директории loops, чтобы он выглядел так:

fn main() {
    loop {
        println!("повтор!");
    }
}

Когда мы запустим эту программу, то увидим постоянный вывод на экран сообщения "повтор!", пока не остановим программу вручную. Большинство терминалов поддерживают комбинацию клавиш Ctrl+C для прерывания работы запущенной программы, зависшей в цикле. Попробуйте:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished dev [unoptimized + debuginfo] target(s) in 0.29s
     Running `target/debug/loops`
повтор!
повтор!
повтор!
повтор!
^Cповтор!

Символ ^C показывает, что вы нажали комбинацию клавиш Ctrl+C. Вы можете увидеть или не увидеть слово "повтор!", напечатанное после ^C, в зависимости от момента в цикле, когда был получен сигнал прерывания.

Rust также предоставляет способ выхода из цикла программно. Вы можете поместить ключевое слово break в теле цикла, чтобы указать программе, когда необходимо установить выполнение цикла. Вспомните, как мы делали подобное в игре по угадыванию числа (см. [2], секция "Выход после правильного угадывания"), чтобы реализовать выход из игры, когда было угадано верное число.

В реализации игры мы также использовали ключевое слово continue внутри тела цикла, что говорит программе пропустить остальные операторы в теле цикла и начать выполнять тело цикла сначала.

Возврат значения из цикла. Одно из применений цикла состоит в повторных попытках операции, которая возможно может окончиться неудачей, например проверка того, завершил ли поток свою работу. Вам также может понадобиться передать результат операций из цикла в остальную часть кода. Для этого можно добавить возвращаемое значение после выражения break, которое используется для остановки цикла. Это значение будет возвращено из цикла, чтобы вы могли его использовать, как показано в следующем примере:

fn main() {
    let mut counter = 0;

let result = loop { counter += 1;

if counter == 10 { break counter * 2; } };

println!("Результат равен {result}"); }

Перед входом в цикл вы декларируете мутируемую переменную counter и инициализируете её в 0. Затем мы декларируем переменную с именем result для хранения значения, возвращенного из цикла. На каждой итерации цикла мы добавляем 1 к переменной counter, и затем проверяем, равно ли 10 значение counter. Когда это так, мы используем ключевое слово break со значением counter * 2. После цикла мы используем точку с запятой, чтобы завершить оператор, который присваивает значение переменной result. В конце программы мы печатаем значение в result, которое в нашем случае равно 20.

Вы также можете выполнить возврат из цикла оператором return. В то время как break только производит выход из текущего цикла, return всегда производит выход из текущей функции.

Метки циклов для устранения неоднозначности между несколькими циклами. Если у вас есть вложенные циклы (когда один цикл находится в теле другого цикла), то break и continue применяются к самому внутреннему циклу, где эти операторы встретились. Вы можете опционально указать метку цикла перед loop, которую можно затем использовать, чтобы оборвать (break) или начать итерацию цикла сначала (continue) по указанной метке вместо применения этих ключевых слов к внутреннему циклу. Метки цикла должны начинаться с одиночной кавычки. Вот пример двух вложенных циклов, когда по метке counting_up обрывается выполнение внешнего цикла, а не внутреннего:

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {count}");
        let mut remaining = 10;

loop { println!("remaining = {remaining}"); if remaining == 9 { break; } if count == 2 { break 'counting_up; } remaining -= 1; }

count += 1; } println!("End count = {count}"); }

Внешнему циклу назначена метка 'counting_up, и он будет выполнять инкремент переменной count от 0 до 2. Внутренний цикл без метки выполняет декремент переменной remaining от 10 до 9. Первый оператор break не указывает метку, поэтому он произведет выход только из внутреннего цикла. Оператор break 'counting_up; произведет выход из внешнего цикла. Этот код напечатает следующее:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished dev [unoptimized + debuginfo] target(s) in 0.58s
     Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2

Условные циклы на основе while. Программе часто нужно вычислять значение условия внутри цикла. Пока это условие true, итерации цикла продолжаются. Когда условие перестает быть true, программа вызывает break, останавливая цикл. Такое поведение можно реализовать комбинацией loop, if, else и break, как мы это уже делали. Однако этот шаблон поведения цикла настолько распространен, что в Rust для этого случая есть специальная конструкция, которая называется циклом while. В листинге 3-3 мы используем while для прокрутки цикла программы 3 раза, в каждой итерации уменьшая значение переменной number, пока она не достигнет нуля, и затем после завершения цикла печатается сообщение и происходит выход.

fn main() {
    let mut number = 3;

while number != 0 { println!("{number}!"); number -= 1; }

println!("LIFTOFF!!!"); }

Листинг 3-3. Использование цикла while для прокрутки итераций тела цикла, пока условие остается true.

Эта конструкция избавляет от лишних вложенных в loop конструкций типа if, else и break, и выглядит чище. Пока условие цикла while вычисляется как true, его код продолжает выполняться; иначе происходит выход из цикла.

Цикл по коллекции на основе for. Также можно использовать while для организации цикла по элементам коллекции, такой как массив. Пример цикла в листинге 3-4 печатает каждый элемент из массива a.

fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

while index < 5 { println!("значение равно: {}", a[index]);

index += 1; } }

Листинг 3-4. Обработка в цикле while элементов коллекции.

Здесь код проходит по элементам массива от первого до последнего. Обработка начинается с индекса index, равного 0, и продолжается, пока не достигнет последнего возможного индекса в массиве (т. е. до тех пор, пока не перестанет быть true выражение index < 5). Запустите этот код, и он напечатает каждый элемент в массиве:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished dev [unoptimized + debuginfo] target(s) in 0.32s
     Running `target/debug/loops`
значение равно: 10
значение равно: 20
значение равно: 30
значение равно: 40
значение равно: 50

Все пять значений были выведены в терминал, как и ожидалось. Хотя index достиг значение 5 в некоторый момент, цикл останавливается до того, как будет сделана попытка выборки из массива по индексу 6.

Однако подобный подход итерации по массиву подвержен ошибкам. Мы можем привести программу в состояние паники, если проверяемое значение index условия цикла окажется некорректным. Например, если вы поменяете определение массива, чтобы он содержал четыре элемента вместо пяти, но при этом забудете поменять условие цикла while на index < 4, то запуск такого кода приведет к панике программы. Это также несколько замедляет программу, потому что компилятор добавляет runtime-код для проверки, вышел ли индекс за пределы массива, выполняемый на каждой итерации цикла.

В качестве более краткой альтернативы можно использовать цикл for и выполнить некоторый код по каждому элементу в коллекции. Цикл for может выглядеть тогда как показано в листинге 3-5.

fn main() {
    let a = [10, 20, 30, 40, 50];

for element in a { println!("значение равно: {element}"); } }

Листинг 3-5. Итерации цикла по каждому элементу в коллекции с помощью for.

Когда мы запустим этот код, то получим тот же вывод, что генерировал код листинга 3-4. Что более важно, теперь мы повысили безопасность своего кода, устранив вероятность ошибок, которые могли возникнуть в результате выхода за пределы конца массива, или недостаточной обработки массива, когда могут быть пропущены какие-то элементы.

При использовании цикла for для итерации по элементам коллекции вам не надо помнить о других изменениях в коде, когда изменяется количество элементов в массиве, по сравнению с методом, который использовался в листинге 3-4.

Безопасность и лаконичность циклов for делают их наиболее часто используемой конструкцией для циклов в Rust. Даже в ситуациях, когда вы хотите запустить некий код определенное количество раз, как в примере обратного отсчета из листинга 3-3, большинство программистов предпочтут использовать цикл for. Есть для этого способ, заключающийся в применении Range, предоставляемого стандартной библиотекой. Range генерирует все числа в последовательности, начиная от одного числа и заканчивая другим числом.

Ниже показан пример обратного отсчета с использованием цикла for и еще одного метода, о котором мы пока не говорили. Это метод rev(), который генерирует обратную последовательность чисел, т. е. диапазон (1..4) будет преобразован в (4..1):

fn main() {
    for number in (1..4).rev() {
        println!("{number}!");
    }
    println!("LIFTOFF!!!");
}

Этот код выглядит приятнее, не так ли?

[Ссылки]

1. Rust Common Programming Concepts site:rust-lang.org.
2. Rust: программирование игры - угадывание числа (глава 2).
3. Rust Constant evaluation site:rust-lang.org.
4. Two's complement site:wikipedia.org.
5. IEEE 754-2008 site:wikipedia.org.

 

Добавить комментарий


Защитный код
Обновить

Top of Page