ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1145
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
-->
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;
|
For more information about this error, try `rustc --explain E0658`. warning: `functions` (bin "functions") generated 1 warning error: could not compile `functions` due to 2 previous errors; 1 warning emitted
присваивания возвращает значение присваивания. В таких языках можно писать код x
= y = 6
и обе переменные x
и y
будут иметь одинаковое значение
6
. Но в Rust не так.
Выражения вычисляют значение и составляют большую часть остального кода, который вы напишете на Rust. Рассмотрим математическую операцию, к примеру
5 + 6
, которая является выражением, вычисляющим значение
11
. Выражения могут быть частью операторов: в листинге 3-1 6
в операторе let y = 6;
является выражением, которое вычисляется в значение
6
. Вызов функции - это выражение. Вызов макроса - это выражение. Новый блок области видимости, созданный с помощью фигурных скобок,
представляет собой выражение, например:
Имя файла: src/main.rs
Это выражение:
является блоком, который в данном случае вычисляется как
4
. Это значение затем привязывается к y
как часть оператора let
. Обратите внимание, что x + 1
не имеет точки с запятой в конце, в отличие от большинства строк, которые вы видели до сих пор.
Выражения не включают точку с запятой в конце. Если добавить точку с запятой в конец выражения, то оно превратится в оператор, и не вернёт значение. Помните об этом,
когда будете изучать возвращаемые функцией значения и выражения.
Функции с возвращаемыми значениями
Функции могут возвращать значения коду, который их вызывает. Мы не называем возвращаемые значения, но мы должны объявить их тип после стрелки (
->
). В Rust возвращаемое значение функции является синонимом значения конечного выражения в блоке тела функции. Вы можете раньше выйти из функции и вернуть значение,
используя ключевое слово return и указав значение, но большинство функций неявно возвращают последнее выражение. Вот пример такой функции:
Имя файла: src/main.rs fn main
() { let y = { let x =
3
; x +
1
}; println!
(
"The value of y is: {y}"
);
}
{ let x =
3
; x +
1
}
= y = 6
и обе переменные x
и y
будут иметь одинаковое значение
6
. Но в Rust не так.
Выражения вычисляют значение и составляют большую часть остального кода, который вы напишете на Rust. Рассмотрим математическую операцию, к примеру
5 + 6
, которая является выражением, вычисляющим значение
11
. Выражения могут быть частью операторов: в листинге 3-1 6
в операторе let y = 6;
является выражением, которое вычисляется в значение
6
. Вызов функции - это выражение. Вызов макроса - это выражение. Новый блок области видимости, созданный с помощью фигурных скобок,
представляет собой выражение, например:
Имя файла: src/main.rs
Это выражение:
является блоком, который в данном случае вычисляется как
4
. Это значение затем привязывается к y
как часть оператора let
. Обратите внимание, что x + 1
не имеет точки с запятой в конце, в отличие от большинства строк, которые вы видели до сих пор.
Выражения не включают точку с запятой в конце. Если добавить точку с запятой в конец выражения, то оно превратится в оператор, и не вернёт значение. Помните об этом,
когда будете изучать возвращаемые функцией значения и выражения.
Функции с возвращаемыми значениями
Функции могут возвращать значения коду, который их вызывает. Мы не называем возвращаемые значения, но мы должны объявить их тип после стрелки (
->
). В Rust возвращаемое значение функции является синонимом значения конечного выражения в блоке тела функции. Вы можете раньше выйти из функции и вернуть значение,
используя ключевое слово return и указав значение, но большинство функций неявно возвращают последнее выражение. Вот пример такой функции:
Имя файла: src/main.rs fn main
() { let y = { let x =
3
; x +
1
}; println!
(
"The value of y is: {y}"
);
}
{ let x =
3
; x +
1
}
В коде функции five нет вызовов функций, макросов или даже операторов let
- есть только одно число
5
. Это является абсолютно корректной функцией в Rust. Заметьте,
что возвращаемый тип у данной функции определён как
-> i32
. Попробуйте запустить этот код. Вывод будет таким:
Значение
5
в five является возвращаемым функцией значением, поэтому возвращаемый тип - i32
. Рассмотрим пример более детально. Здесь есть два важных момента: во-первых, строка let x = five();
показывает использование возвращаемого функцией значения для инициализации переменной. Так как функция five возвращает
5
, то эта строка эквивалентна следующей:
Во-вторых, у функции five нет параметров и определён тип возвращаемого значения,
но тело функции представляет собой одинокую
5
без точки с запятой, потому что это выражение, значение которого мы хотим вернуть.
Рассмотрим другой пример:
Имя файла: src/main.rs
Запуск кода напечатает
The value of x is: 6
. Но если поставить точку с запятой в конце строки, содержащей x + 1
, превратив её из выражения в оператор, мы получим fn five
() -> i32
{
5
} fn main
() { let x = five(); println!
(
"The value of x is: {x}"
);
}
$
cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/functions`
The value of x is: 5 let x =
5
; fn main
() { let x = plus_one(
5
); println!
(
"The value of x is: {x}"
);
} fn plus_one
(x: i32
) -> i32
{ x +
1
}
ошибку.
Имя файла: src/main.rs
Компиляция данного кода вызывает следующую ошибку:
Основное сообщение об ошибке "несовпадение типов" раскрывает ключевую проблему этого кода. Определение функции plus_one сообщает, что будет возвращено i32
, но операторы не вычисляют значение, что и выражается единичным типом
()
Следовательно, ничего не возвращается, что противоречит определению функции и приводит к ошибке. В этом выводе Rust выдаёт сообщение, которое, возможно, поможет исправить эту проблему: он предлагает удалить точку с запятой для устранения ошибки.
fn main
() { let x = plus_one(
5
); println!
(
"The value of x is: {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
For more information about this error, try `rustc --explain E0308`. error: could not compile `functions` due to previous error
Имя файла: src/main.rs
Компиляция данного кода вызывает следующую ошибку:
Основное сообщение об ошибке "несовпадение типов" раскрывает ключевую проблему этого кода. Определение функции plus_one сообщает, что будет возвращено i32
, но операторы не вычисляют значение, что и выражается единичным типом
()
Следовательно, ничего не возвращается, что противоречит определению функции и приводит к ошибке. В этом выводе Rust выдаёт сообщение, которое, возможно, поможет исправить эту проблему: он предлагает удалить точку с запятой для устранения ошибки.
fn main
() { let x = plus_one(
5
); println!
(
"The value of x is: {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
For more information about this error, try `rustc --explain E0308`. error: could not compile `functions` due to previous error
Комментарии
Все хорошие программисты, создавая программный код, стремятся сделать его простым для понимания. Бывают всё же случаи, когда дополнительное описание просто необходимо. В этих случаях программисты пишут заметки (или как их ещё называют,
комментарии). Комментарии игнорируются компилятором, но для тех кто код читает - это очень важная часть документации.
Пример простого комментария:
В Rust комментарии должны начинаться двумя символами
//
и простираются до конца строки. Чтобы комментарии поместились на более чем одной строке, необходимо разместить
//
на каждой строке, как в примере:
Комментарии могут быть размещены в конце строки имеющей код:
Файл: src/main.rs
Но чаще вы увидите их использование в следующем формате, когда комментарий размещён на отдельной строке над кодом, который комментируется:
Файл: src/main.rs
Также в Rust есть другой тип комментариев - документирующие комментарии, которые мы обсудим в разделе "Публикация пакета на Crates.io" Главы 14.
// Hello, world.
// So we’re doing something complicated here, long enough that we need
// multiple lines of comments to do it! Whew! Hopefully, this comment will
// explain what’s going on.
fn main
() { let lucky_number =
7
;
// I’m feeling lucky today
} fn main
() {
// I’m feeling lucky today let lucky_number =
7
;
}
Управляющие конструкции
Способность запускать некоторый код в зависимости от истинности условия или выполнять некоторый код многократно, пока условие истинно, является базовым элементом большинства языков программирования. Наиболее распространёнными конструкциями, позволяющими управлять потоком выполнения кода в Rust, являются выражения if и циклы.
Выражения if
Выражение if позволяет разветвлять код в зависимости от условий. Вы задаёте условие, а затем объявляете: "Если это условие соблюдено, то выполнить этот блок кода.
Если условие не соблюдается, не выполнять этот блок кода".
Для изучения выражения if создайте новый проект под названием branches в каталоге
projects. В файл src/main.rs поместите следующий код:
Имя файла: src/main.rs
Все выражения if начинаются с ключевого слова if
, за которым следует условие. В
этом случае условие проверяет, имеет ли переменная number значение меньше 5. Если условие истинно, мы помещаем блок исполняемого кода сразу после условия внутри фигурных скобок. Блоки кода, связанные с условиями в выражениях if
, иногда называются ответвлениями, точно так же, как ответвления в выражениях match
,
которые мы обсуждали в разделе
«Сравнение догадки с секретным числом»
Главы 2.
Опционально можно включить выражение else
, которое мы используем в данном примере, чтобы предоставить программе альтернативный блок выполнения кода,
выполняющийся при ложном условии. Если не указать выражение else и условие будет ложным, программа просто пропустит блок if и перейдёт к следующему фрагменту кода.
Попробуйте запустить этот код. Появится следующий результат:
fn main
() { let number =
3
; if number <
5
{ println!
(
"condition was true"
);
} else
{ println!
(
"condition was false"
);
}
}
Попробуйте изменить значение number на значение, которое делает условие false и
посмотрите, что произойдёт:
Запустите программу снова и посмотрите на вывод:
Также стоит отметить, что условие в этом коде должно быть логического типа bool
. Если условие не является bool
, возникнет ошибка. Например, попробуйте запустить следующий код:
Имя файла: src/main.rs
На этот раз условие if вычисляется в значение
3
, и Rust бросает ошибку:
Ошибка говорит, что Rust ожидал тип bool
, но получил значение целочисленного типа.
В отличии от других языков вроде Ruby и JavaScript, Rust не будет пытаться автоматически конвертировать нелогические типы в логические. Необходимо явно и всегда использовать if с логическим типом в качестве условия. Если нужно, чтобы блок
$
cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches` condition was true 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` condition was false fn main
() { let number =
3
; if number { println!
(
"number was three"
);
}
}
$
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` due to previous error
кода if запускался только, когда число не равно
0
, то, например, мы можем изменить выражение if на следующее:
Имя файла: src/main.rs
Будет напечатана следующая строка number was something other than zero
Обработка нескольких условий с помощью else if
Можно использовать несколько условий, комбинируя if и else в выражении else if
Например:
Имя файла: src/main.rs
У этой программы есть четыре возможных пути выполнения. После её запуска вы должны увидеть следующий результат:
Во время выполнения этой программы по очереди проверяется каждое выражение if и
выполняется первое тело, для которого условие истинно. Заметьте, что хотя 6 делится на
2, мы не видим ни вывода number is divisible by 2
, ни текста number is not divisible by 4, 3, or 2
из блока else
. Так происходит потому, что Rust выполняет блок только для первого истинного условия, а обнаружив его, даже не проверяет остальные.
fn main
() { let number =
3
; if number !=
0
{ println!
(
"number was something other than zero"
);
}
} fn main
() { let number =
6
; if number %
4
==
0
{ println!
(
"number is divisible by 4"
);
} else if number %
3
==
0
{ println!
(
"number is divisible by 3"
);
} else if number %
2
==
0
{ println!
(
"number is divisible by 2"
);
} else
{ println!
(
"number is not divisible by 4, 3, or 2"
);
}
}
$
cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches` number is divisible by 3
0
, то, например, мы можем изменить выражение if на следующее:
Имя файла: src/main.rs
Будет напечатана следующая строка number was something other than zero
Обработка нескольких условий с помощью else if
Можно использовать несколько условий, комбинируя if и else в выражении else if
Например:
Имя файла: src/main.rs
У этой программы есть четыре возможных пути выполнения. После её запуска вы должны увидеть следующий результат:
Во время выполнения этой программы по очереди проверяется каждое выражение if и
выполняется первое тело, для которого условие истинно. Заметьте, что хотя 6 делится на
2, мы не видим ни вывода number is divisible by 2
, ни текста number is not divisible by 4, 3, or 2
из блока else
. Так происходит потому, что Rust выполняет блок только для первого истинного условия, а обнаружив его, даже не проверяет остальные.
fn main
() { let number =
3
; if number !=
0
{ println!
(
"number was something other than zero"
);
}
} fn main
() { let number =
6
; if number %
4
==
0
{ println!
(
"number is divisible by 4"
);
} else if number %
3
==
0
{ println!
(
"number is divisible by 3"
);
} else if number %
2
==
0
{ println!
(
"number is divisible by 2"
);
} else
{ println!
(
"number is not divisible by 4, 3, or 2"
);
}
}
$
cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches` number is divisible by 3
Использование множества выражений else if приводит к загромождению кода,
поэтому при наличии более чем одного выражения, возможно, стоит провести рефакторинг кода. В главе 6 описана мощная конструкция ветвления Rust для таких случаев, называемая match
Использование if в let-операторах
Поскольку if является выражением, его можно использовать в правой части оператора let для присвоения результата переменной, как в листинге 3-2.
Имя файла: src/main.rs
Листинг 3-2: Присвоение результата выражения
if
переменной
Переменная number будет привязана к значению, которое является результатом выражения if
. Запустим код и посмотрим, что происходит:
Вспомните, что блоки кода вычисляются последним выражением в них, а числа сами по себе также являются выражениями. В данном случае, значение всего выражения if зависит от того, какой блок выполняется. При этом значения, которые могут быть результатами каждого из ветвей if
, должны быть одного типа. В Листинге 3-2,
результатами обеих ветвей if и else являются целочисленный тип i32
. Если типы не совпадают, как в следующем примере, мы получим ошибку:
Имя файла: src/main.rs
При попытке компиляции этого кода, мы получим ошибку. Ветви if и else представляют несовместимые типы значений, и Rust точно указывает, где искать fn main
() { let condition = true
; let number = if condition {
5
} else
{
6
}; println!
(
"The value of number is: {number}"
);
}
$
cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/branches`
The value of number is: 5 fn main
() { let condition = true
; let number = if condition {
5
} else
{
"six"
}; println!
(
"The value of number is: {number}"
);
}
проблему в программе:
Выражение в блоке if вычисляется как целочисленное, а выражение в блоке else вычисляется как строка. Это не сработает, потому что переменные должны иметь один тип, а Rust должен знать во время компиляции, какого типа переменная number
. Зная тип number
, компилятор может убедиться, что тип действителен везде, где мы используем number
. Rust не смог бы этого сделать, если бы тип number определялся только во время выполнения. Компилятор усложнился бы и давал бы меньше гарантий в отношении кода, если бы ему приходилось отслеживать несколько гипотетических типов для любой переменной.
Повторение выполнения кода с помощью циклов
Часто бывает полезно выполнить блок кода более одного раза. Для этой задачи Rust предоставляет несколько циклов, которые позволяют выполнить код внутри тела цикла до конца, а затем сразу же вернуться в начало. Для экспериментов с циклами давайте создадим новый проект под названием loops.
В Rust есть три вида циклов: loop
, while и for
. Давайте попробуем каждый из них.
Повторение выполнения кода с помощью loop
Ключевое слово loop говорит Rust выполнять блок кода снова и снова до бесконечности или пока не будет явно приказано остановиться.
В качестве примера, измените код файла src/main.rs в каталоге проекта loops на код ниже:
Имя файла: src/main.rs
$
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` due to previous error fn main
() { loop
{ println!
(
"again!"
);
}
}
Выражение в блоке if вычисляется как целочисленное, а выражение в блоке else вычисляется как строка. Это не сработает, потому что переменные должны иметь один тип, а Rust должен знать во время компиляции, какого типа переменная number
. Зная тип number
, компилятор может убедиться, что тип действителен везде, где мы используем number
. Rust не смог бы этого сделать, если бы тип number определялся только во время выполнения. Компилятор усложнился бы и давал бы меньше гарантий в отношении кода, если бы ему приходилось отслеживать несколько гипотетических типов для любой переменной.
Повторение выполнения кода с помощью циклов
Часто бывает полезно выполнить блок кода более одного раза. Для этой задачи Rust предоставляет несколько циклов, которые позволяют выполнить код внутри тела цикла до конца, а затем сразу же вернуться в начало. Для экспериментов с циклами давайте создадим новый проект под названием loops.
В Rust есть три вида циклов: loop
, while и for
. Давайте попробуем каждый из них.
Повторение выполнения кода с помощью loop
Ключевое слово loop говорит Rust выполнять блок кода снова и снова до бесконечности или пока не будет явно приказано остановиться.
В качестве примера, измените код файла src/main.rs в каталоге проекта loops на код ниже:
Имя файла: src/main.rs
$
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` due to previous error fn main
() { loop
{ println!
(
"again!"
);
}
}
Когда запустим эту программу, увидим, как again!
печатается снова и снова, пока не остановить программу вручную. Большинство терминалов поддерживают комбинацию клавиш ctrl-c для прерывания программы, которая застряла в непрерывном цикле.
Попробуйте:
Символ
^C
обозначает место, где было нажато ctrl-c . В зависимости от того, где находился код в цикле в момент получения сигнала прерывания, вы можете увидеть или не увидеть слово again!
, напечатанное после
^C
К счастью, Rust также предоставляет способ выйти из цикла с помощью кода. Ключевое слово break нужно поместить в цикл, чтобы указать программе, когда следует прекратить выполнение цикла. Напоминаем, мы делали так в игре "Угадайка" в разделе "Выход после правильной догадки"
Главы 2, чтобы выйти из программы, когда пользователь выиграл игру, угадав правильное число.
Мы также использовали continue в игре "Угадайка", которая указывает программе в цикле пропустить весь оставшийся код в данной итерации цикла и перейти к следующей итерации.
Возвращение значений из циклов
Одно из применений loop
- это повторение операции, которая может закончиться неудачей, например, проверка успешности выполнения потоком своего задания. Также может понадобиться передать из цикла результат этой операции в остальную часть кода.
Для этого можно добавить возвращаемое значение после выражения break
, которое используется для остановки цикла. Это значение будет возвращено из цикла, и его можно будет использовать, как показано здесь:
$
cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.29s
Running `target/debug/loops` again! again! again! again!
^Cagain!
Перед циклом мы объявляем переменную с именем counter и инициализируем её
значением
0
. Затем мы объявляем переменную с именем result для хранения значения, возвращаемого из цикла. На каждой итерации цикла мы добавляем
1
к переменной counter
, а затем проверяем, равен ли счётчик
10
. Когда это происходит,
мы используем ключевое слово break со значением counter * 2
. После цикла мы ставим точку с запятой для завершения инструкции, присваивающей значение result
Наконец, мы выводим значение в result
, равное в данном случае 20.
1 2 3 4 5 6 7 8 9 ... 62
Метки циклов для устранения неоднозначности между несколькими циклами
Если у вас есть циклы внутри циклов, break и continue применяются к самому внутреннему циклу в этой цепочке. При желании вы можете создать метку цикла,
которую вы затем сможете использовать с break или continue для указания, что эти ключевые слова применяются к помеченному циклу, а не к самому внутреннему циклу.
Метки цикла должны начинаться с одинарной кавычки. Вот пример с двумя вложенными циклами:
fn main
() { let mut counter =
0
; let result = loop
{ counter +=
1
; if counter ==
10
{ break counter *
2
;
}
}; println!
(
"The result is {result}"
);
}
Внешний цикл имеет метку 'counting_up
, и он будет считать от 0 до 2. Внутренний цикл без метки ведёт обратный отсчёт от 10 до 9. Первый break
, который не содержит метку,
выйдет только из внутреннего цикла. Оператор break 'counting_up;
завершит внешний цикл. Этот код напечатает:
Циклы с условием while
В программе часто требуется проверить состояние условия в цикле. Пока условие истинно, цикл выполняется. Когда условие перестаёт быть истинным, программа вызывает break
, останавливая цикл. Такое поведение можно реализовать с помощью комбинации loop
, if
, else и break
. При желании попробуйте сделать это в программе. Это настолько распространённый паттерн, что в Rust реализована встроенная языковая конструкция для него, называемая цикл while
. В листинге 3-3 мы используем while
, чтобы выполнить три цикла программы, производя каждый раз обратный отсчёт, а затем, после завершения цикла, печатаем сообщение и выходим.
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}"
);
}
$
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
Имя файла: src/main.rs
Листинг 3-3: Использование цикла
while
для выполнения кода, пока условие истинно
Эта конструкция устраняет множество вложений, которые потребовались бы при использовании loop
, if
, else и break
, и она более понятна. Пока условие истинно, код выполняется, в противном случае происходит выход из цикла.
Цикл по элементам коллекции с помощью for
Для перебора элементов коллекции, например, массива, можно использовать конструкцию while
. Например, цикл в листинге 3-4 печатает каждый элемент массива a
Имя файла: src/main.rs
Листинг 3-4: Перебор каждого элемента коллекции с помощью цикла
while
Этот код выполняет перебор элементов массива. Он начинается с индекса
0
, а затем циклически выполняется, пока не достигнет последнего индекса в массиве (то есть, когда index < 5
уже не является истиной). Выполнение этого кода напечатает каждый элемент массива:
fn main
() { let mut number =
3
; while number !=
0
{ println!
(
"{number}!"
); number -=
1
;
} println!
(
"LIFTOFF!!!"
);
} fn main
() { let a = [
10
,
20
,
30
,
40
,
50
]; let mut index =
0
; while index <
5
{ println!
(
"the value is: {}"
, a[index]); index +=
1
;
}
}
Все пять значений массива появляются в терминале, как и ожидалось. Поскольку index в какой-то момент достигнет значения
5
, цикл прекратит выполнение перед попыткой извлечь шестое значение из массива.
Однако такой подход чреват ошибками. Можно вызвать панику в программе, если значение индекса или условие теста будут неверны. Например, если изменить определение массива a
на четыре элемента, но забыть обновить условие на while index < 4
, код вызовет панику. Также это медленно, поскольку компилятор добавляет код времени выполнения для обеспечения проверки нахождения индекса в границах массива на каждой итерации цикла.
В качестве более краткой альтернативы можно использовать цикл for и выполнять некоторый код для каждого элемента коллекции. Цикл for может выглядеть как код в листинге 3-5.
Имя файла: src/main.rs
Листинг 3-5: Перебор каждого элемента коллекции с помощью цикла
for
При выполнении этого кода мы увидим тот же результат, что и в листинге 3-4. Что важнее, теперь мы повысили безопасность кода и устранили вероятность ошибок,
которые могут возникнуть в результате выхода за пределы массива или недостаточно далёкого перехода и пропуска некоторых элементов.
При использовании цикла for не нужно помнить о внесении изменений в другой код, в случае изменения количества значений в массиве, как это было бы с методом,
использованным в листинге 3-4.
Безопасность и компактность циклов for делают их наиболее часто используемой конструкцией цикла в Rust. Даже в ситуациях необходимости выполнения некоторого кода определённое количество раз, как в примере обратного отсчёта, в котором
$
cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.32s
Running `target/debug/loops` the value is: 10 the value is: 20 the value is: 30 the value is: 40 the value is: 50 fn main
() { let a = [
10
,
20
,
30
,
40
,
50
]; for element in a { println!
(
"the value is: {element}"
);
}
}
использовался цикл while из Листинга 3-3, большинство Rustaceans использовали бы цикл for
. Для этого можно использовать
Range
, предоставляемый стандартной библиотекой, который генерирует последовательность всех чисел, начиная с первого числа и заканчивая вторым числом, но не включая его (т.е.
(1..4)
эквивалентно
[1, 2,
3]
или в общем случае
(start..end)
эквивалентно
[start, start+1, start+2, ... , end-2, end-1]
- прим.переводчика).
Вот как будет выглядеть обратный отсчёт с использованием цикла for и другого метода,
о котором мы ещё не говорили, rev
, для разворота диапазона:
Имя файла: src/main.rs
Данный код выглядит лучше, не так ли?
Итоги
Вы справились! Это была объёмная глава: вы узнали о переменных, скалярных и составных типах данных, функциях, комментариях, выражениях if и циклах! Для практики работы с концепциями, обсуждаемыми в этой главе, попробуйте создать программы для выполнения следующих действий:
Конвертация температур между значениями по Фаренгейту к Цельсия.
Генерирование n-го числа Фибоначчи.
Распечатайте текст рождественской песни "Двенадцать дней Рождества",
воспользовавшись повторами в песне.
Когда вы будете готовы двигаться дальше, мы поговорим о концепции в Rust, которая не
существует обычно в других языках программирования: владение.
fn main
() { for number in
(
1 4
).rev() { println!
(
"{number}!"
);
} println!
(
"LIFTOFF!!!"
);
}
. Для этого можно использовать
Range
, предоставляемый стандартной библиотекой, который генерирует последовательность всех чисел, начиная с первого числа и заканчивая вторым числом, но не включая его (т.е.
(1..4)
эквивалентно
[1, 2,
3]
или в общем случае
(start..end)
эквивалентно
[start, start+1, start+2, ... , end-2, end-1]
- прим.переводчика).
Вот как будет выглядеть обратный отсчёт с использованием цикла for и другого метода,
о котором мы ещё не говорили, rev
, для разворота диапазона:
Имя файла: src/main.rs
Данный код выглядит лучше, не так ли?
Итоги
Вы справились! Это была объёмная глава: вы узнали о переменных, скалярных и составных типах данных, функциях, комментариях, выражениях if и циклах! Для практики работы с концепциями, обсуждаемыми в этой главе, попробуйте создать программы для выполнения следующих действий:
Конвертация температур между значениями по Фаренгейту к Цельсия.
Генерирование n-го числа Фибоначчи.
Распечатайте текст рождественской песни "Двенадцать дней Рождества",
воспользовавшись повторами в песне.
Когда вы будете готовы двигаться дальше, мы поговорим о концепции в Rust, которая не
существует обычно в других языках программирования: владение.
fn main
() { for number in
(
1 4
).rev() { println!
(
"{number}!"
);
} println!
(
"LIFTOFF!!!"
);
}
Понимание Владения
Владение - это самая уникальная особенность Rust, которая имеет глубокие последствия для всего языка. Это позволяет Rust обеспечивать безопасность памяти без использования сборщика мусора, поэтому важно понимать, как работает владение. В
этой главе мы поговорим о владении, а также о нескольких связанных с ним возможностях: заимствовании, срезах и о том, как Rust раскладывает данные в памяти.
Что такое владение?
Владение — это набор правил, определяющих, как программа на Rust управляет памятью. Все программы так или иначе должны использовать память компьютера во время работы. В некоторых языках есть сборщики мусора, которые регулярно отслеживают неиспользуемую память во время работы программы; в других программист должен память явно выделять и освобождать. В Rust используется третий подход: управление памятью происходит через систему владения с набором правил,
которые проверяются компилятором. При нарушении любого из правил программа не будет скомпилирована. Ни одна из особенностей владения не замедлит работу вашей программы.
Поскольку владение является новой концепцией для многих программистов, требуется некоторое время, чтобы привыкнуть к ней. Хорошая новость заключается в том, что чем больше у вас будет опыта с Rust и с правилами системы владения, тем легче вам будет естественным образом разрабатывать безопасный и эффективный код. Держитесь! Не сдавайтесь!
Понимание концепции владения даст вам основу для понимания всех остальных особенностей, делающих Rust уникальным. В этой главе вы изучите владение на примерах, которые сфокусированы на наиболее часто используемой структуре данных:
строках.
Стек и куча
Многие языки программирования не требуют, чтобы вы слишком часто думали о стеке и куче. Но в языках системного программирования, одним из которых является Rust, то, находится значение в стеке или в куче, влияет на поведение языка и на принятие вами определённых решений. Владение будет описано через призму стека и кучи позже в этой главе, а пока — краткое пояснение.
И стек, и куча — это части памяти, доступные вашему коду для использования во время выполнения. Однако они структурированы по-разному. Стек хранит значения в порядке их получения, а удаляет — в обратном. Это называется «последний
пришёл, первый вышел». Подумайте о стопке тарелок: когда вы добавляете тарелки,
вы кладёте их сверху стопки — когда вам нужна тарелка, вы берёте одну так же сверху. Добавление или удаление тарелок посередине или снизу не сработает!
Добавление данных называется помещением в стек, а удаление — извлечением из
стека. Все данные, хранящиеся в стеке, должны иметь известный фиксированный размер. Данные, размер которых во время компиляции неизвестен или может измениться, должны храниться в куче.
Куча менее организованна: когда вы помещаете данные в кучу, вы запрашиваете определённое место для их хранения. Распределитель памяти находит подходящее
пустое место в куче, помечает его как используемое и возвращает указатель —
адрес этого места. Этот процесс называется выделением в куче и иногда сокращённо просто выделением (помещение значений в стек не считается выделением).
Поскольку указатель на кучу имеет известный фиксированный размер, вы можете хранить указатель в стеке, но когда вам нужны фактические данные, вы должны следовать за указателем. Представьте, что вы сидите в ресторане. Когда вы входите,
вы называете количество человек в вашей группе, и персонал находит свободный стол, который подходит всем, и ведёт вас туда. Если кто-то из вашей группы опоздает, он может спросить, где вы сидели, чтобы найти вас.
Размещение в стек происходит быстрее, чем выделение в куче, потому что операционная система не тратит время на поиски места для хранения данных.
Местом размещения всегда является верхушка стека. Выделение памяти в куче требует больше работы, потому что операционная система должна сначала найти достаточно большой участок памяти для хранения данных и затем выполнить резервирование, чтобы подготовится к следующему выделению.
Доступ к данным в куче медленнее, чем доступ к данным в стеке, потому что вам нужно следовать по адресу указателя, чтобы добраться туда. Современные процессоры работают быстрее, если они меньше прыгают по памяти. Продолжая аналогию, рассмотрим официанта в ресторане, принимающего заказы со многих столов. Наиболее эффективно будет получить все заказы за одним столом, прежде чем переходить к следующему столу. Получение заказа из таблицы А, затем из таблицы В, затем снова одного из А, а затем снова одного из В было бы гораздо более медленным делом. Точно так же процессор может выполнять свою работу лучше, если он работает с данными, которые находятся близко к другим данным
(как в стеке), а не дальше (как это может быть в куче).
Когда ваш код вызывает функцию, значения, переданные в неё (потенциально включающие указатели на данные в куче), и локальные переменные помещаются в стек. Когда функция завершается, эти значения извлекаются из стека.
Отслеживание того, какие части кода используют какие данные, минимизация количества дублирующихся данных и очистка неиспользуемых данных в куче,
чтобы не исчерпать пространство, — все эти проблемы решает владение. Как только вы поймёте, что такое владение, вам не нужно будет слишком часто думать о стеке и куче. Однако знание того, что основная цель владения — управление данными кучи, может помочь объяснить, почему оно работает именно так.
Правила владения
Во-первых, давайте взглянем на правила владения. Помните об этих правилах, пока мы работаем с примерами, которые их иллюстрируют:
У каждого значения в Rust есть владелец,
адрес этого места. Этот процесс называется выделением в куче и иногда сокращённо просто выделением (помещение значений в стек не считается выделением).
Поскольку указатель на кучу имеет известный фиксированный размер, вы можете хранить указатель в стеке, но когда вам нужны фактические данные, вы должны следовать за указателем. Представьте, что вы сидите в ресторане. Когда вы входите,
вы называете количество человек в вашей группе, и персонал находит свободный стол, который подходит всем, и ведёт вас туда. Если кто-то из вашей группы опоздает, он может спросить, где вы сидели, чтобы найти вас.
Размещение в стек происходит быстрее, чем выделение в куче, потому что операционная система не тратит время на поиски места для хранения данных.
Местом размещения всегда является верхушка стека. Выделение памяти в куче требует больше работы, потому что операционная система должна сначала найти достаточно большой участок памяти для хранения данных и затем выполнить резервирование, чтобы подготовится к следующему выделению.
Доступ к данным в куче медленнее, чем доступ к данным в стеке, потому что вам нужно следовать по адресу указателя, чтобы добраться туда. Современные процессоры работают быстрее, если они меньше прыгают по памяти. Продолжая аналогию, рассмотрим официанта в ресторане, принимающего заказы со многих столов. Наиболее эффективно будет получить все заказы за одним столом, прежде чем переходить к следующему столу. Получение заказа из таблицы А, затем из таблицы В, затем снова одного из А, а затем снова одного из В было бы гораздо более медленным делом. Точно так же процессор может выполнять свою работу лучше, если он работает с данными, которые находятся близко к другим данным
(как в стеке), а не дальше (как это может быть в куче).
Когда ваш код вызывает функцию, значения, переданные в неё (потенциально включающие указатели на данные в куче), и локальные переменные помещаются в стек. Когда функция завершается, эти значения извлекаются из стека.
Отслеживание того, какие части кода используют какие данные, минимизация количества дублирующихся данных и очистка неиспользуемых данных в куче,
чтобы не исчерпать пространство, — все эти проблемы решает владение. Как только вы поймёте, что такое владение, вам не нужно будет слишком часто думать о стеке и куче. Однако знание того, что основная цель владения — управление данными кучи, может помочь объяснить, почему оно работает именно так.
Правила владения
Во-первых, давайте взглянем на правила владения. Помните об этих правилах, пока мы работаем с примерами, которые их иллюстрируют:
У каждого значения в Rust есть владелец,