ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1134
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
обработать; эта функциональность может предотвратить ошибки, которые чрезвычайно распространены в других языках программирования.
Дизайн языка программирования часто рассматривается с точки зрения того, какие функции вы включаете в него, но те функции, которые вы исключаете, также важны.
Например в Rust нет такого функционала как null значения, однако он есть во многих других языках. Null значение - это значение, которое означает, что значения нет. В языках с null значением переменные всегда могут находиться в одном из двух состояний: нет
значения (null) или есть значение (not-null).
В своей презентации 2009 года «Null ссылки: ошибка в миллиард долларов» Тони Хоар
(Tony Hoare), изобретатель null, сказал следующее:
Я называю это своей ошибкой на миллиард долларов. В то время я разрабатывал первую комплексную систему типов для ссылок на объектно-ориентированном языке. Моя цель состояла в том, чтобы гарантировать, что любое использование ссылок должно быть абсолютно безопасным, с автоматической проверкой компилятором. Но я не мог устоять перед соблазном вставить пустую ссылку просто потому, что это было так легко реализовать. Это привело к бесчисленным ошибкам,
уязвимостям и системным сбоям, которые, вероятно, причинили боль и ущерб на миллиард долларов за последние сорок лет.
Проблема с null значениями заключается в том, что если вы попытаетесь использовать null значение в качестве not-null значения, вы получите ошибку определённого рода.
Поскольку свойство null или not-null распространено повсеместно, сделать такую ошибку очень просто.
Тем не менее, концепция, которую null пытается выразить, является полезной: null - это значение, которое в настоящее время по какой-то причине недействительно или отсутствует.
Проблема с null значениями заключается в том, что если вы попытаетесь использовать null значение как не-null значение, вы получите какую-то ошибку. Поскольку это свойство null или не-null широко распространено, очень легко совершить такую ошибку.
Перечисление
Option
настолько полезно, что оно даже включено в прелюдию; вам не нужно явно вводить его в область видимости. Его варианты также включены в прелюдию: вы можете использовать
Some и
None напрямую, без префикса
Option::
При всём при этом,
Option
является обычным перечислением, а
Some(T)
и
None представляют собой его варианты.
enum
Option
{
None
,
Some
(T),
}
Дизайн языка программирования часто рассматривается с точки зрения того, какие функции вы включаете в него, но те функции, которые вы исключаете, также важны.
Например в Rust нет такого функционала как null значения, однако он есть во многих других языках. Null значение - это значение, которое означает, что значения нет. В языках с null значением переменные всегда могут находиться в одном из двух состояний: нет
значения (null) или есть значение (not-null).
В своей презентации 2009 года «Null ссылки: ошибка в миллиард долларов» Тони Хоар
(Tony Hoare), изобретатель null, сказал следующее:
Я называю это своей ошибкой на миллиард долларов. В то время я разрабатывал первую комплексную систему типов для ссылок на объектно-ориентированном языке. Моя цель состояла в том, чтобы гарантировать, что любое использование ссылок должно быть абсолютно безопасным, с автоматической проверкой компилятором. Но я не мог устоять перед соблазном вставить пустую ссылку просто потому, что это было так легко реализовать. Это привело к бесчисленным ошибкам,
уязвимостям и системным сбоям, которые, вероятно, причинили боль и ущерб на миллиард долларов за последние сорок лет.
Проблема с null значениями заключается в том, что если вы попытаетесь использовать null значение в качестве not-null значения, вы получите ошибку определённого рода.
Поскольку свойство null или not-null распространено повсеместно, сделать такую ошибку очень просто.
Тем не менее, концепция, которую null пытается выразить, является полезной: null - это значение, которое в настоящее время по какой-то причине недействительно или отсутствует.
Проблема с null значениями заключается в том, что если вы попытаетесь использовать null значение как не-null значение, вы получите какую-то ошибку. Поскольку это свойство null или не-null широко распространено, очень легко совершить такую ошибку.
Перечисление
Option
настолько полезно, что оно даже включено в прелюдию; вам не нужно явно вводить его в область видимости. Его варианты также включены в прелюдию: вы можете использовать
Some и
None напрямую, без префикса
Option::
При всём при этом,
Option
является обычным перечислением, а
Some(T)
и
None представляют собой его варианты.
enum
Option
None
,
Some
(T),
}
Проблема не в самой концепции, а в конкретной реализации. Таким образом, в Rust нет null-значений, но есть перечисление, которое может закодировать концепцию наличия или отсутствия значения. Это перечисление
Option
и оно определено в стандартной библиотеке следующим образом:
Тип some_number
-
Option
. Тип some_string
-
Option<&str>
, это другой тип. Rust может вывести эти типы, потому что мы указали значение внутри варианта
Some
. Для absent_number
Rust требует, чтобы мы аннотировали общий тип для
Option
: компилятор не может вывести тип, который будет в
Some
, глядя только на значение
None
. Здесь мы сообщаем Rust, что absent_number должен иметь тип
Option
Когда есть значение
Some
, мы знаем, что значение присутствует и содержится внутри
Some
. Когда есть значение
None
, это означает то же самое, что и null в некотором смысле: у нас нет действительного значения. Так почему наличие
Option
лучше, чем null?
Вкратце, поскольку
Option
и
T
(где
T
может быть любым типом) относятся к разным типам, компилятор не позволит нам использовать значение
Option
даже если бы оно было определённо допустимым вариантом
Some
. Например, этот код не будет компилироваться, потому что он пытается добавить i8
к значению типа
Option
:
Запуск данного кода даст ошибку ниже:
let some_number =
Some
(
5
); let some_char =
Some
(
'e'
); let absent_number:
Option
<
i32
> =
None
; let x: i8
=
5
; let y:
Option
<
i8
> =
Some
(
5
); let sum = x + y;
Сильно! Фактически, это сообщение об ошибке означает, что Rust не понимает, как сложить i8
и
Option
, потому что это разные типы. Когда у нас есть значение типа на подобие i8
, компилятор гарантирует, что у нас всегда есть допустимое значение типа. Мы можем уверенно продолжать работу, не проверяя его на null перед использованием. Однако, когда у нас есть значение типа
Option
(где
T
- это любое значение любого типа
T
, упакованное в
Option
, например значение типа i8
или
String
), мы должны беспокоиться о том, что значение типа T возможно не имеет значения (является вариантом
None
), и компилятор позаботится о том, чтобы мы обработали такой случай, прежде чем мы бы попытались использовать
None значение.
Другими словами, вы должны преобразовать
Option
в
T
прежде чем вы сможете выполнять операции с этим
T
. Как правило, это помогает выявить одну из наиболее распространённых проблем с null: предполагая, что что-то не равно null, когда оно на самом деле равно null.
Устранение риска ошибочного предположения касательно не-null значения помогает вам быть более уверенным в своём коде. Чтобы иметь значение, которое может быть null, вы должны явно описать тип этого значения с помощью
Option
. Затем, когда вы используете это значение, вы обязаны явно обрабатывать случай, когда значение равно null. Везде, где значение имеет тип, отличный от
Option
, вы можете смело рассчитывать на то, что значение не равно null. Это продуманное проектное решение в
Rust, ограничивающее распространение null и увеличивающее безопасность кода на
Rust.
Итак, как же получить значение
T
из варианта
Some
, если у вас на руках есть только объект
Option
, и как можно его, вообще, использовать? Перечисление
Option
имеет большое количество методов, полезных в различных ситуациях; вы можете
$
cargo run
Compiling enums v0.1.0 (file:///projects/enums) error[E0277]: cannot add `Option
-->
src/main.rs:5:17
|
5 | let sum = x + y;
| ^ no implementation for `i8 + Option
|
= help: the trait `Add
ознакомиться с ними в его документации
. Знакомство с методами перечисления
Option
будет чрезвычайно полезным в вашем путешествии с Rust.
В общем случае, чтобы использовать значение
Option
, нужен код, который будет обрабатывать все варианты перечисления
Option
. Вам понадобится некоторый код,
который будет работать только тогда, когда у вас есть значение
Some(T)
, и этому коду разрешено использовать внутреннее
T
. Также вам понадобится другой код, который будет работать, если у вас есть значение
None
, и у этого кода не будет доступного значения
T
. Выражение match
— это конструкция управления потоком выполнения программы, которая делает именно это при работе с перечислениями: она запускает разный код в зависимости от того, какой вариант перечисления имеется, и этот код может использовать данные, находящиеся внутри совпавшего варианта.
. Знакомство с методами перечисления
Option
будет чрезвычайно полезным в вашем путешествии с Rust.
В общем случае, чтобы использовать значение
Option
, нужен код, который будет обрабатывать все варианты перечисления
Option
. Вам понадобится некоторый код,
который будет работать только тогда, когда у вас есть значение
Some(T)
, и этому коду разрешено использовать внутреннее
T
. Также вам понадобится другой код, который будет работать, если у вас есть значение
None
, и у этого кода не будет доступного значения
T
. Выражение match
— это конструкция управления потоком выполнения программы, которая делает именно это при работе с перечислениями: она запускает разный код в зависимости от того, какой вариант перечисления имеется, и этот код может использовать данные, находящиеся внутри совпавшего варианта.
1 ... 8 9 10 11 12 13 14 15 ... 62
Управляющая конструкция match
В Rust есть чрезвычайно мощный механизм управления потоком, именуемый match
,
который позволяет сравнивать значение с различными шаблонами и затем выполнять код в зависимости от того, какой из шаблонов совпал. Шаблоны могут состоять из литеральных значений, имён переменных, подстановочных знаков и многого другого; в главе 18 рассматриваются все различные виды шаблонов и то, что они делают. Сила match заключается в выразительности шаблонов и в том, что компилятор проверяет, что все возможные случаи обработаны.
Думайте о выражении match как о машине для сортировки монет: монеты скользят по дорожке с различными по размеру отверстиями, и каждая монета падает через первое попавшееся отверстие, в которое она поместилась. Таким же образом значения проходят через каждый шаблон в match
, и при первом же "подходящем" шаблоне значение попадает в соответствующий блок кода, который будет использоваться во время выполнения.
Говоря о монетах, давайте используем их в качестве примера, используя match
! Для этого мы напишем функцию, которая будет получать на вход неизвестную монету
Соединённых Штатов и, подобно счётной машине, определять, какая это монета, и возвращать её стоимость в центах, как показано в листинге 6-3.
Листинг 6-3: Перечисление и выражение
match
, использующее в качестве шаблонов его варианты
Давайте разберём match в функции value_in_cents
. Сначала мы используем ключевое слово match
, за которым следует выражение, которое в данном случае является значением coin
. Это кажется очень похожим на выражение, используемое в if
, но есть большая разница: в if выражение должно возвращать булево значение, а здесь оно может возвращать любой тип. Типом coin в данном примере является перечисление
Coin
, которое мы определили в первой строке.
enum
Coin
{
Penny,
Nickel,
Dime,
Quarter,
} fn value_in_cents
(coin: Coin) -> u8
{ match coin {
Coin::Penny =>
1
,
Coin::Nickel =>
5
,
Coin::Dime =>
10
,
Coin::Quarter =>
25
,
}
}
Далее идут ветки match
. Ветки состоят из двух частей: шаблон и некоторый код. Здесь первая ветка имеет шаблон, который является значением
Coin::Penny
, затем идёт оператор
=>
, который разделяет шаблон и код для выполнения. Код в этом случае - это просто значение
1
. Каждая ветка отделяется от последующей при помощи запятой.
Когда выполняется выражение match
, оно сравнивает полученное значение с образцом каждой ветки по порядку. Если шаблон совпадает со значением, то выполняется код,
связанный с этим шаблоном. Если этот шаблон не соответствует значению, то выполнение продолжается со следующей ветки, так же, как в автомате по сортировке монет. У нас может быть столько веток, сколько нужно: в листинге 6-3 наш match состоит из четырёх веток.
Код, связанный с каждой веткой, является выражением, а полученное значение выражения в соответствующей ветке — это значение, которое возвращается для всего выражения match
Обычно фигурные скобки не используются, если код совпадающей ветви невелик, как в листинге 6-3, где каждая ветвь просто возвращает значение. Если вы хотите выполнить несколько строк кода в одной ветви, вы должны использовать фигурные скобки, а запятая после этой ветви необязательна. Например, следующий код печатает "Lucky penny!" каждый раз, когда метод вызывается с
Coin::Penny
, но при этом он возвращает последнее значение блока -
1
:
Шаблоны, которые привязывают значения
Есть ещё одно полезное качество у веток в выражении match
: они могут привязываться к частям тех значений, которые совпали с шаблоном. Благодаря этому можно извлекать значения из вариантов перечисления.
В качестве примера, давайте изменим один из вариантов перечисления так, чтобы он хранил в себе данные. С 1999 по 2008 год Соединённые Штаты чеканили 25 центов с различным дизайном на одной стороне для каждого из 50 штатов. Ни одна другая монета не получила дизайна штата, только четверть доллара имела эту дополнительную особенность. Мы можем добавить эту информацию в наш enum путём изменения варианта
Quarter и включить в него значение
UsState
, как сделано в листинге 6-4.
fn value_in_cents
(coin: Coin) -> u8
{ match coin {
Coin::Penny => { println!
(
"Lucky penny!"
);
1
}
Coin::Nickel =>
5
,
Coin::Dime =>
10
,
Coin::Quarter =>
25
,
}
}
Листинг 6-4: Перечисление
Coin
, в котором вариант
Quarter
также сохраняет значение
UsState
Представьте, что ваш друг пытается собрать четвертаки всех 50 штатов. Сортируя монеты по типу, мы также будем сообщать название штата, к которому относится каждый четвертак, чтобы, если у нашего друга нет такой монеты, он мог добавить её в свою коллекцию.
В выражении match для этого кода мы добавляем переменную с именем state в
шаблон, который соответствует значениям варианта
Coin::Quarter
. Когда
Coin::Quarter совпадёт с шаблоном, переменная state будет привязана к значению штата этого четвертака. Затем мы сможем использовать state в коде этой ветки, вот так:
Если мы сделаем вызов функции value_in_cents(Coin::Quarter(UsState::Alaska))
, то coin будет иметь значение
Coin::Quarter(UsState::Alaska)
. Когда мы будем сравнивать это значение с каждой из веток, ни одна из них не будет совпадать, пока мы не достигнем варианта
Coin::Quarter(state)
. В этот момент state привяжется к значению
UsState::Alaska
. Затем мы сможем использовать эту привязку в выражении println!
, получив таким образом внутреннее значение варианта
Quarter перечисления
Coin
Сопоставление шаблона для Option
#[derive(Debug)]
// so we can inspect the state in a minute enum
UsState
{
Alabama,
Alaska,
// --snip--
} enum
Coin
{
Penny,
Nickel,
Dime,
Quarter(UsState),
} fn value_in_cents
(coin: Coin) -> u8
{ match coin {
Coin::Penny =>
1
,
Coin::Nickel =>
5
,
Coin::Dime =>
10
,
Coin::Quarter(state) => { println!
(
"State quarter from {:?}!"
, state);
25
}
}
}
В предыдущем разделе мы хотели получить внутреннее значение
T
для случая
Some при использовании
Option
; мы можем обработать тип
Option
используя match
,
как уже делали с перечислением
Coin
! Вместо сравнения монет мы будем сравнивать варианты
Option
, независимо от этого изменения механизм работы выражения match останется прежним.
Допустим, мы хотим написать функцию, которая принимает
Option
и если есть значение внутри, то добавляет 1 к существующему значению. Если значения нет, то функция должна возвращать значение
None и не пытаться выполнить какие-либо операции.
Такую функцию довольно легко написать благодаря выражению match
, код будет выглядеть как в листинге 6-5.
Листинг 6-5: Функция, использующая выражение
match
для
Option
Давайте более подробно рассмотрим первое выполнение plus_one
. Когда мы вызываем plus_one(five)
, переменная x
в теле plus_one будет иметь значение
Some(5)
. Затем мы сравниваем это значение с каждой ветвью выражения match.
Значение
Some(5)
не соответствует шаблону
None
, поэтому мы продолжаем со следующей ветки.
Совпадает ли
Some(5)
с шаблоном
Some(i)
? Да, это так! У нас такой же вариант. Тогда переменная i
привязывается к значению, содержащемуся внутри
Some
, поэтому i
получает значение
5
. Затем выполняется код ассоциированный для данной ветки,
поэтому мы добавляем 1 к значению i
и создаём новое значение
Some со значением
6
внутри.
Теперь давайте рассмотрим второй вызов plus_one в листинге 6-5, где x
является
None
Мы входим в выражение match и сравниваем значение с первой веткой.
fn plus_one
(x:
Option
<
i32
>) ->
Option
<
i32
> { match x {
None
=>
None
,
Some
(i) =>
Some
(i +
1
),
}
} let five =
Some
(
5
); let six = plus_one(five); let none = plus_one(
None
);
None
=>
None
,
Some
(i) =>
Some
(i +
1
),
None
=>
None
,
Оно совпадает! Для данной ветки шаблон (None) не подразумевает наличие какого-то значения к которому можно было бы что-то добавить, поэтому программа останавливается и возвращает значение которое находится справа от
=>
- т.е.
None
. Так как шаблон первой ветки совпал, то никакие другие шаблоны веток не сравниваются.
Комбинирование match и перечислений полезно во многих ситуациях. Вы часто будете видеть подобную комбинацию в коде на Rust: сделать сопоставление значений перечисления используя match
, привязать переменную к данным внутри значения,
выполнить код на основе привязанных данных. Сначала это может показаться немного сложным, но как только вы привыкнете, то захотите чтобы такая возможность была бы во всех языках. Это неизменно любимый пользователями приём.
Match объемлет все варианты значения
Есть ещё один аспект match
, который мы должны обсудить: шаблоны должны покрывать все возможные варианты. Рассмотрим эту версию нашей функции plus_one
, которая содержит ошибку и не компилируется:
Мы не обработали вариант
None
, поэтому этот код вызовет дефект в программе. К
счастью, Rust знает и умеет ловить такой случай. Если мы попытаемся скомпилировать такой код, мы получим ошибку компиляции:
fn plus_one
(x:
Option
<
i32
>) ->
Option
<
i32
> { match x {
Some
(i) =>
Some
(i +
1
),
}
}
$
cargo run
Compiling enums v0.1.0 (file:///projects/enums) error[E0004]: non-exhaustive patterns: `None` not covered
-->
src/main.rs:3:15
|
3 | match x {
| ^ pattern `None` not covered
| note: `Option` defined here
= note: the matched value is of type `Option` help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
4 Some(i) => Some(i + 1),
5 None => todo!(),
|
For more information about this error, try `rustc --explain E0004`. error: could not compile `enums` due to previous error
Rust знает, что мы не описали все возможные случаи, и даже знает, какой именно из шаблонов мы упустили! Сопоставления в Rust являются исчерпывающими: мы должны покрыть все возможные варианты, чтобы код был корректным. Особенно в случае
Option
, когда Rust не даёт нам забыть обработать явным образом значение
None
,
тем самым он защищает нас от предположения, что у нас есть значение, в то время как у нас может быть и null, что делает невозможным совершить ошибку на миллиард долларов, о которой говорилось ранее.
Универсальные шаблоны и заполнитель _
Используя перечисления, мы также можем выполнять специальные действия для нескольких определённых значений, а для всех остальных значений выполнять одно действие по умолчанию. Представьте, что мы реализуем игру, в которой при выпадении
3 игрок не двигается, а получает новую модную шляпу. Если выпадает 7, игрок теряет шляпу. При всех остальных значениях ваш игрок перемещается на столько-то мест на игровом поле. Вот match
, реализующий эту логику, в котором результат броска костей жёстко закодирован, а не является случайным значением, а вся остальная логика представлена функциями без тел, поскольку их реализация не входит в рамки данного примера:
Для первых двух веток шаблонами являются литеральные значения 3 и 7. Для последней ветки, которая охватывает все остальные возможные значения, шаблоном является переменная, которую мы решили назвать other
. Код, выполняемый для другой ветки,
использует эту переменную, передавая её в функцию move_player.
Этот код компилируется, даже если мы не перечислили все возможные значения u8
,
потому что последний паттерн будет соответствовать всем значениям, не указанным в конкретном списке. Этот универсальный шаблон удовлетворяет требованию, что соответствие должно быть исчерпывающим. Обратите внимание, что мы должны поместить ветку с универсальным шаблоном последней, потому что шаблоны оцениваются по порядку. Rust предупредит нас, если мы добавим ветки после универсального шаблона, потому что эти последующие ветки никогда не будут выполняться!
let dice_roll =
9
; match dice_roll {
3
=> add_fancy_hat(),
7
=> remove_fancy_hat(), other => move_player(other),
} fn add_fancy_hat
() {} fn remove_fancy_hat
() {} fn move_player
(num_spaces: u8
) {}
В Rust также есть шаблон, который можно использовать, когда мы не хотим использовать значение в универсальном шаблоне:
_
, который является специальным шаблоном, который соответствует любому значению и не привязывается к этому значению. Это говорит Rust, что мы не собираемся использовать это значение, поэтому
Rust не будет предупреждать нас о неиспользуемой переменной.
Давайте изменим правила игры так: если выпадает что-то, кроме 3 или 7, нужно бросить ещё раз. Нам не нужно использовать значение в этом случае, поэтому мы можем изменить наш код, чтобы использовать
_
вместо переменной с именем other
:
Этот пример также удовлетворяет требованию исчерпывающей полноты, поскольку мы явно игнорируем все остальные значения в последней ветке; мы ничего не забыли.
Если мы изменим правила игры ещё раз, чтобы в ваш ход не происходило ничего другого, если вы бросаете не 3 или 7, мы можем выразить это, используя единичное значение (пустой тип кортежа, о котором мы упоминали в разделе "Кортежи"
) в качестве кода, который идёт вместе с веткой
_
:
Здесь мы явно говорим Rust, что не собираемся использовать никакое другое значение,
которое не соответствует шаблонам в предыдущих ветках, и не хотим запускать никакой код в этом случае.
Подробнее о шаблонах и совпадениях мы поговорим в
Главе 18
. Пока же мы перейдём к синтаксису if let
, который может быть полезен в ситуациях, когда выражение match слишком многословно.
let dice_roll =
9
; match dice_roll {
3
=> add_fancy_hat(),
7
=> remove_fancy_hat(),
_ => reroll(),
} fn add_fancy_hat
() {} fn remove_fancy_hat
() {} fn reroll
() {} let dice_roll =
9
; match dice_roll {
3
=> add_fancy_hat(),
7
=> remove_fancy_hat(),
_ => (),
} fn add_fancy_hat
() {} fn remove_fancy_hat
() {}
Компактное управление потоком выполнения с if let
Синтаксис if let позволяет скомбинировать if и let в менее многословную конструкцию, и затем обработать значения соответствующе только одному шаблону,
одновременно игнорируя все остальные. Рассмотрим программу в листинге 6-6, которая обрабатывает сопоставление значения
Option
в переменной config_max
, но хочет выполнить код только в том случае, если значение является вариантом
Some
Листинг 6-6. Выражение
match
, которое выполнит код только при значении равном
Some
Если значение равно
Some
, мы распечатываем значение в варианте
Some
, привязывая значение к переменной max в шаблоне. Мы не хотим ничего делать со значением
None
Чтобы удовлетворить выражение match
, мы должны добавить
_ => ()
после обработки первой и единственной ветки, и добавление шаблонного кода раздражает.
Вместо этого, мы могли бы написать это более коротким способом, используя if let
Следующий код ведёт себя так же, как выражение match в листинге 6-6:
Синтаксис if let принимает шаблон и выражение, разделённые знаком равенства. if let сработает так же, как match
, когда в него на вход передадут выражение и подходящим шаблоном для этого выражения окажется первая ветка. В нашем случае шаблоном является
Some(max)
, где max привязывается к значению внутри
Some
. Затем мы можем использовать max в теле блока if let так же, как мы использовали max в
соответствующей ветке match
. Код в блоке if let не запускается, если значение не соответствует шаблону.
Используя if let мы меньше печатаем, меньше делаем отступов и меньше получаем шаблонного кода. Тем не менее, мы теряем полную проверку всех вариантов,
предоставляемую выражением match
. Выбор между match и if let зависит от того, что вы делаете в вашем конкретном случае и является ли получение краткости при потере полноты проверки подходящим компромиссом.
Другими словами, вы можете думать о конструкции if let как о синтаксическом сахаре
для match
, который выполнит код если входное значение будет соответствовать единственному шаблону, и проигнорирует все остальные значения.
let config_max =
Some
(
3u8
); match config_max {
Some
(max) => println!
(
"The maximum is configured to be {}"
, max),
_ => (),
} let config_max =
Some
(
3u8
); if let
Some
(max) = config_max { println!
(
"The maximum is configured to be {}"
, max);
}
Можно добавлять else к if let
. Блок кода, который находится внутри else аналогичен по смыслу блоку кода ветки связанной с шаблоном
_
выражения match
(которое эквивалентно сборной конструкции if let и else
). Вспомним объявление перечисления
Coin в листинге 6-4, где вариант
Quarter также содержит внутри значение штата типа
UsState
. Если бы мы хотели посчитать все монеты не являющиеся четвертями, а для четвертей печатать название штата, то мы могли бы сделать это с помощью выражения match таким образом:
Или мы могли бы использовать выражение if let и else так:
Если у вас есть ситуация в которой ваша программа имеет логику которая слишком многословна для того чтобы её выражать используя match
, помните, о том, что также в вашем наборе инструментов Rust есть if let
Итоги
Мы рассмотрели как использовать перечисления для создания пользовательских типов,
которые могут быть одним из наборов перечисляемых значений. Мы показали, как тип
Option
из стандартной библиотеки помогает использовать систему типов для предотвращения ошибок. А когда значения перечисления имеют данные внутри них,
можно использовать match или if let
, чтобы извлечь и пользоваться значением, в зависимости от того, сколько случаев нужно обработать.
Теперь ваши программы на Rust могут выражать концепции вашей предметной области используя структуры и перечисления. Создание и использование пользовательских типов в API обеспечивает типобезопасность: компилятор позаботится о том, чтобы функции получали значения только того типа, который они ожидают.
Чтобы предоставить вашим пользователям хорошо организованный API, который прост в использовании и предоставляет только то, что нужно вашим пользователям, надо поговорить о модулях в Rust.
let mut count =
0
; match coin {
Coin::Quarter(state) => println!
(
"State quarter from {:?}!"
, state),
_ => count +=
1
,
} let mut count =
0
; if let
Coin::Quarter(state) = coin { println!
(
"State quarter from {:?}!"
, state);
} else
{ count +=
1
;
}
1 ... 9 10 11 12 13 14 15 16 ... 62
В Rust есть чрезвычайно мощный механизм управления потоком, именуемый match
,
который позволяет сравнивать значение с различными шаблонами и затем выполнять код в зависимости от того, какой из шаблонов совпал. Шаблоны могут состоять из литеральных значений, имён переменных, подстановочных знаков и многого другого; в главе 18 рассматриваются все различные виды шаблонов и то, что они делают. Сила match заключается в выразительности шаблонов и в том, что компилятор проверяет, что все возможные случаи обработаны.
Думайте о выражении match как о машине для сортировки монет: монеты скользят по дорожке с различными по размеру отверстиями, и каждая монета падает через первое попавшееся отверстие, в которое она поместилась. Таким же образом значения проходят через каждый шаблон в match
, и при первом же "подходящем" шаблоне значение попадает в соответствующий блок кода, который будет использоваться во время выполнения.
Говоря о монетах, давайте используем их в качестве примера, используя match
! Для этого мы напишем функцию, которая будет получать на вход неизвестную монету
Соединённых Штатов и, подобно счётной машине, определять, какая это монета, и возвращать её стоимость в центах, как показано в листинге 6-3.
Листинг 6-3: Перечисление и выражение
match
, использующее в качестве шаблонов его варианты
Давайте разберём match в функции value_in_cents
. Сначала мы используем ключевое слово match
, за которым следует выражение, которое в данном случае является значением coin
. Это кажется очень похожим на выражение, используемое в if
, но есть большая разница: в if выражение должно возвращать булево значение, а здесь оно может возвращать любой тип. Типом coin в данном примере является перечисление
Coin
, которое мы определили в первой строке.
enum
Coin
{
Penny,
Nickel,
Dime,
Quarter,
} fn value_in_cents
(coin: Coin) -> u8
{ match coin {
Coin::Penny =>
1
,
Coin::Nickel =>
5
,
Coin::Dime =>
10
,
Coin::Quarter =>
25
,
}
}
Далее идут ветки match
. Ветки состоят из двух частей: шаблон и некоторый код. Здесь первая ветка имеет шаблон, который является значением
Coin::Penny
, затем идёт оператор
=>
, который разделяет шаблон и код для выполнения. Код в этом случае - это просто значение
1
. Каждая ветка отделяется от последующей при помощи запятой.
Когда выполняется выражение match
, оно сравнивает полученное значение с образцом каждой ветки по порядку. Если шаблон совпадает со значением, то выполняется код,
связанный с этим шаблоном. Если этот шаблон не соответствует значению, то выполнение продолжается со следующей ветки, так же, как в автомате по сортировке монет. У нас может быть столько веток, сколько нужно: в листинге 6-3 наш match состоит из четырёх веток.
Код, связанный с каждой веткой, является выражением, а полученное значение выражения в соответствующей ветке — это значение, которое возвращается для всего выражения match
Обычно фигурные скобки не используются, если код совпадающей ветви невелик, как в листинге 6-3, где каждая ветвь просто возвращает значение. Если вы хотите выполнить несколько строк кода в одной ветви, вы должны использовать фигурные скобки, а запятая после этой ветви необязательна. Например, следующий код печатает "Lucky penny!" каждый раз, когда метод вызывается с
Coin::Penny
, но при этом он возвращает последнее значение блока -
1
:
Шаблоны, которые привязывают значения
Есть ещё одно полезное качество у веток в выражении match
: они могут привязываться к частям тех значений, которые совпали с шаблоном. Благодаря этому можно извлекать значения из вариантов перечисления.
В качестве примера, давайте изменим один из вариантов перечисления так, чтобы он хранил в себе данные. С 1999 по 2008 год Соединённые Штаты чеканили 25 центов с различным дизайном на одной стороне для каждого из 50 штатов. Ни одна другая монета не получила дизайна штата, только четверть доллара имела эту дополнительную особенность. Мы можем добавить эту информацию в наш enum путём изменения варианта
Quarter и включить в него значение
UsState
, как сделано в листинге 6-4.
fn value_in_cents
(coin: Coin) -> u8
{ match coin {
Coin::Penny => { println!
(
"Lucky penny!"
);
1
}
Coin::Nickel =>
5
,
Coin::Dime =>
10
,
Coin::Quarter =>
25
,
}
}
Листинг 6-4: Перечисление
Coin
, в котором вариант
Quarter
также сохраняет значение
UsState
Представьте, что ваш друг пытается собрать четвертаки всех 50 штатов. Сортируя монеты по типу, мы также будем сообщать название штата, к которому относится каждый четвертак, чтобы, если у нашего друга нет такой монеты, он мог добавить её в свою коллекцию.
В выражении match для этого кода мы добавляем переменную с именем state в
шаблон, который соответствует значениям варианта
Coin::Quarter
. Когда
Coin::Quarter совпадёт с шаблоном, переменная state будет привязана к значению штата этого четвертака. Затем мы сможем использовать state в коде этой ветки, вот так:
Если мы сделаем вызов функции value_in_cents(Coin::Quarter(UsState::Alaska))
, то coin будет иметь значение
Coin::Quarter(UsState::Alaska)
. Когда мы будем сравнивать это значение с каждой из веток, ни одна из них не будет совпадать, пока мы не достигнем варианта
Coin::Quarter(state)
. В этот момент state привяжется к значению
UsState::Alaska
. Затем мы сможем использовать эту привязку в выражении println!
, получив таким образом внутреннее значение варианта
Quarter перечисления
Coin
Сопоставление шаблона для Option
#[derive(Debug)]
// so we can inspect the state in a minute enum
UsState
{
Alabama,
Alaska,
// --snip--
} enum
Coin
{
Penny,
Nickel,
Dime,
Quarter(UsState),
} fn value_in_cents
(coin: Coin) -> u8
{ match coin {
Coin::Penny =>
1
,
Coin::Nickel =>
5
,
Coin::Dime =>
10
,
Coin::Quarter(state) => { println!
(
"State quarter from {:?}!"
, state);
25
}
}
}
В предыдущем разделе мы хотели получить внутреннее значение
T
для случая
Some при использовании
Option
; мы можем обработать тип
Option
используя match
,
как уже делали с перечислением
Coin
! Вместо сравнения монет мы будем сравнивать варианты
Option
, независимо от этого изменения механизм работы выражения match останется прежним.
Допустим, мы хотим написать функцию, которая принимает
Option
и если есть значение внутри, то добавляет 1 к существующему значению. Если значения нет, то функция должна возвращать значение
None и не пытаться выполнить какие-либо операции.
Такую функцию довольно легко написать благодаря выражению match
, код будет выглядеть как в листинге 6-5.
Листинг 6-5: Функция, использующая выражение
match
для
Option
Давайте более подробно рассмотрим первое выполнение plus_one
. Когда мы вызываем plus_one(five)
, переменная x
в теле plus_one будет иметь значение
Some(5)
. Затем мы сравниваем это значение с каждой ветвью выражения match.
Значение
Some(5)
не соответствует шаблону
None
, поэтому мы продолжаем со следующей ветки.
Совпадает ли
Some(5)
с шаблоном
Some(i)
? Да, это так! У нас такой же вариант. Тогда переменная i
привязывается к значению, содержащемуся внутри
Some
, поэтому i
получает значение
5
. Затем выполняется код ассоциированный для данной ветки,
поэтому мы добавляем 1 к значению i
и создаём новое значение
Some со значением
6
внутри.
Теперь давайте рассмотрим второй вызов plus_one в листинге 6-5, где x
является
None
Мы входим в выражение match и сравниваем значение с первой веткой.
fn plus_one
(x:
Option
<
i32
>) ->
Option
<
i32
> { match x {
None
=>
None
,
Some
(i) =>
Some
(i +
1
),
}
} let five =
Some
(
5
); let six = plus_one(five); let none = plus_one(
None
);
None
=>
None
,
Some
(i) =>
Some
(i +
1
),
None
=>
None
,
Оно совпадает! Для данной ветки шаблон (None) не подразумевает наличие какого-то значения к которому можно было бы что-то добавить, поэтому программа останавливается и возвращает значение которое находится справа от
=>
- т.е.
None
. Так как шаблон первой ветки совпал, то никакие другие шаблоны веток не сравниваются.
Комбинирование match и перечислений полезно во многих ситуациях. Вы часто будете видеть подобную комбинацию в коде на Rust: сделать сопоставление значений перечисления используя match
, привязать переменную к данным внутри значения,
выполнить код на основе привязанных данных. Сначала это может показаться немного сложным, но как только вы привыкнете, то захотите чтобы такая возможность была бы во всех языках. Это неизменно любимый пользователями приём.
Match объемлет все варианты значения
Есть ещё один аспект match
, который мы должны обсудить: шаблоны должны покрывать все возможные варианты. Рассмотрим эту версию нашей функции plus_one
, которая содержит ошибку и не компилируется:
Мы не обработали вариант
None
, поэтому этот код вызовет дефект в программе. К
счастью, Rust знает и умеет ловить такой случай. Если мы попытаемся скомпилировать такой код, мы получим ошибку компиляции:
fn plus_one
(x:
Option
<
i32
>) ->
Option
<
i32
> { match x {
Some
(i) =>
Some
(i +
1
),
}
}
$
cargo run
Compiling enums v0.1.0 (file:///projects/enums) error[E0004]: non-exhaustive patterns: `None` not covered
-->
src/main.rs:3:15
|
3 | match x {
| ^ pattern `None` not covered
| note: `Option
= note: the matched value is of type `Option
|
4 Some(i) => Some(i + 1),
5 None => todo!(),
|
For more information about this error, try `rustc --explain E0004`. error: could not compile `enums` due to previous error
Rust знает, что мы не описали все возможные случаи, и даже знает, какой именно из шаблонов мы упустили! Сопоставления в Rust являются исчерпывающими: мы должны покрыть все возможные варианты, чтобы код был корректным. Особенно в случае
Option
, когда Rust не даёт нам забыть обработать явным образом значение
None
,
тем самым он защищает нас от предположения, что у нас есть значение, в то время как у нас может быть и null, что делает невозможным совершить ошибку на миллиард долларов, о которой говорилось ранее.
Универсальные шаблоны и заполнитель _
Используя перечисления, мы также можем выполнять специальные действия для нескольких определённых значений, а для всех остальных значений выполнять одно действие по умолчанию. Представьте, что мы реализуем игру, в которой при выпадении
3 игрок не двигается, а получает новую модную шляпу. Если выпадает 7, игрок теряет шляпу. При всех остальных значениях ваш игрок перемещается на столько-то мест на игровом поле. Вот match
, реализующий эту логику, в котором результат броска костей жёстко закодирован, а не является случайным значением, а вся остальная логика представлена функциями без тел, поскольку их реализация не входит в рамки данного примера:
Для первых двух веток шаблонами являются литеральные значения 3 и 7. Для последней ветки, которая охватывает все остальные возможные значения, шаблоном является переменная, которую мы решили назвать other
. Код, выполняемый для другой ветки,
использует эту переменную, передавая её в функцию move_player.
Этот код компилируется, даже если мы не перечислили все возможные значения u8
,
потому что последний паттерн будет соответствовать всем значениям, не указанным в конкретном списке. Этот универсальный шаблон удовлетворяет требованию, что соответствие должно быть исчерпывающим. Обратите внимание, что мы должны поместить ветку с универсальным шаблоном последней, потому что шаблоны оцениваются по порядку. Rust предупредит нас, если мы добавим ветки после универсального шаблона, потому что эти последующие ветки никогда не будут выполняться!
let dice_roll =
9
; match dice_roll {
3
=> add_fancy_hat(),
7
=> remove_fancy_hat(), other => move_player(other),
} fn add_fancy_hat
() {} fn remove_fancy_hat
() {} fn move_player
(num_spaces: u8
) {}
В Rust также есть шаблон, который можно использовать, когда мы не хотим использовать значение в универсальном шаблоне:
_
, который является специальным шаблоном, который соответствует любому значению и не привязывается к этому значению. Это говорит Rust, что мы не собираемся использовать это значение, поэтому
Rust не будет предупреждать нас о неиспользуемой переменной.
Давайте изменим правила игры так: если выпадает что-то, кроме 3 или 7, нужно бросить ещё раз. Нам не нужно использовать значение в этом случае, поэтому мы можем изменить наш код, чтобы использовать
_
вместо переменной с именем other
:
Этот пример также удовлетворяет требованию исчерпывающей полноты, поскольку мы явно игнорируем все остальные значения в последней ветке; мы ничего не забыли.
Если мы изменим правила игры ещё раз, чтобы в ваш ход не происходило ничего другого, если вы бросаете не 3 или 7, мы можем выразить это, используя единичное значение (пустой тип кортежа, о котором мы упоминали в разделе "Кортежи"
) в качестве кода, который идёт вместе с веткой
_
:
Здесь мы явно говорим Rust, что не собираемся использовать никакое другое значение,
которое не соответствует шаблонам в предыдущих ветках, и не хотим запускать никакой код в этом случае.
Подробнее о шаблонах и совпадениях мы поговорим в
Главе 18
. Пока же мы перейдём к синтаксису if let
, который может быть полезен в ситуациях, когда выражение match слишком многословно.
let dice_roll =
9
; match dice_roll {
3
=> add_fancy_hat(),
7
=> remove_fancy_hat(),
_ => reroll(),
} fn add_fancy_hat
() {} fn remove_fancy_hat
() {} fn reroll
() {} let dice_roll =
9
; match dice_roll {
3
=> add_fancy_hat(),
7
=> remove_fancy_hat(),
_ => (),
} fn add_fancy_hat
() {} fn remove_fancy_hat
() {}
Компактное управление потоком выполнения с if let
Синтаксис if let позволяет скомбинировать if и let в менее многословную конструкцию, и затем обработать значения соответствующе только одному шаблону,
одновременно игнорируя все остальные. Рассмотрим программу в листинге 6-6, которая обрабатывает сопоставление значения
Option
в переменной config_max
, но хочет выполнить код только в том случае, если значение является вариантом
Some
Листинг 6-6. Выражение
match
, которое выполнит код только при значении равном
Some
Если значение равно
Some
, мы распечатываем значение в варианте
Some
, привязывая значение к переменной max в шаблоне. Мы не хотим ничего делать со значением
None
Чтобы удовлетворить выражение match
, мы должны добавить
_ => ()
после обработки первой и единственной ветки, и добавление шаблонного кода раздражает.
Вместо этого, мы могли бы написать это более коротким способом, используя if let
Следующий код ведёт себя так же, как выражение match в листинге 6-6:
Синтаксис if let принимает шаблон и выражение, разделённые знаком равенства. if let сработает так же, как match
, когда в него на вход передадут выражение и подходящим шаблоном для этого выражения окажется первая ветка. В нашем случае шаблоном является
Some(max)
, где max привязывается к значению внутри
Some
. Затем мы можем использовать max в теле блока if let так же, как мы использовали max в
соответствующей ветке match
. Код в блоке if let не запускается, если значение не соответствует шаблону.
Используя if let мы меньше печатаем, меньше делаем отступов и меньше получаем шаблонного кода. Тем не менее, мы теряем полную проверку всех вариантов,
предоставляемую выражением match
. Выбор между match и if let зависит от того, что вы делаете в вашем конкретном случае и является ли получение краткости при потере полноты проверки подходящим компромиссом.
Другими словами, вы можете думать о конструкции if let как о синтаксическом сахаре
для match
, который выполнит код если входное значение будет соответствовать единственному шаблону, и проигнорирует все остальные значения.
let config_max =
Some
(
3u8
); match config_max {
Some
(max) => println!
(
"The maximum is configured to be {}"
, max),
_ => (),
} let config_max =
Some
(
3u8
); if let
Some
(max) = config_max { println!
(
"The maximum is configured to be {}"
, max);
}
Можно добавлять else к if let
. Блок кода, который находится внутри else аналогичен по смыслу блоку кода ветки связанной с шаблоном
_
выражения match
(которое эквивалентно сборной конструкции if let и else
). Вспомним объявление перечисления
Coin в листинге 6-4, где вариант
Quarter также содержит внутри значение штата типа
UsState
. Если бы мы хотели посчитать все монеты не являющиеся четвертями, а для четвертей печатать название штата, то мы могли бы сделать это с помощью выражения match таким образом:
Или мы могли бы использовать выражение if let и else так:
Если у вас есть ситуация в которой ваша программа имеет логику которая слишком многословна для того чтобы её выражать используя match
, помните, о том, что также в вашем наборе инструментов Rust есть if let
Итоги
Мы рассмотрели как использовать перечисления для создания пользовательских типов,
которые могут быть одним из наборов перечисляемых значений. Мы показали, как тип
Option
из стандартной библиотеки помогает использовать систему типов для предотвращения ошибок. А когда значения перечисления имеют данные внутри них,
можно использовать match или if let
, чтобы извлечь и пользоваться значением, в зависимости от того, сколько случаев нужно обработать.
Теперь ваши программы на Rust могут выражать концепции вашей предметной области используя структуры и перечисления. Создание и использование пользовательских типов в API обеспечивает типобезопасность: компилятор позаботится о том, чтобы функции получали значения только того типа, который они ожидают.
Чтобы предоставить вашим пользователям хорошо организованный API, который прост в использовании и предоставляет только то, что нужно вашим пользователям, надо поговорить о модулях в Rust.
let mut count =
0
; match coin {
Coin::Quarter(state) => println!
(
"State quarter from {:?}!"
, state),
_ => count +=
1
,
} let mut count =
0
; if let
Coin::Quarter(state) = coin { println!
(
"State quarter from {:?}!"
, state);
} else
{ count +=
1
;
}
1 ... 9 10 11 12 13 14 15 16 ... 62
Управление растущими проектами с
помощью пакетов, крейтов и модулей
По мере роста кодовой базы ваших программ, организация проекта будет иметь большое значение, ведь отслеживание всей программы в голове будет становиться всё
более сложным. Группируя связанные функции и разделяя код по основным функциональностям (фичам, feature), вы делаете более прозрачным понимание о том, где искать код реализующий определённую функцию и где стоит вносить изменения для того чтобы изменить её поведение.
Программы, которые мы писали до сих пор, были в одном файле одного модуля. По мере роста проекта, мы можем организовывать код иначе, разделив его на несколько модулей и несколько файлов. Пакет может содержать несколько бинарных крейтов и опционально один крейт библиотеки. Пакет может включать в себя много бинарных крейтов и опционально один библиотечный крейт. По мере роста пакета вы можете извлекать части программы в отдельные крейты, которые затем станут внешними зависимостями для основного кода нашей программы. Эта глава охватывает все эти техники. В свою очередь для очень крупных проектов, состоящих из набора взаимосвязанных пакетов развивающихся вместе, Cargo предоставляет рабочие пространства, workspaces, их мы рассмотрим за пределами данной главы, в разделе "Рабочие пространства Cargo"
Главы 14.
Мы также обсудим инкапсуляцию деталей, которая позволяет использовать код снова на более высоком уровне: единожды реализовав какую-то операцию, другой код может вызывать этот код через публичный интерфейс, не зная как работает реализация. То, как вы пишете код, определяет какие части общедоступны для использования другим кодом и какие части являются закрытыми деталями реализации для которых вы оставляете право на изменения только за собой. Это ещё один способ ограничить количество деталей, которые вы должны держать в голове.
Связанное понятие - это область видимости: вложенный контекст в котором написан код имеющий набор имён, которые определены «в текущей области видимости». При чтении, письме и компиляции кода, программистам и компиляторам необходимо знать,
относится ли конкретное имя в определённом месте к переменной, к функции, к структуре, к перечислению, к модулю, к константе или другому элементу и что означает этот элемент. Можно создавать области видимости и изменять какие имена входят или выходят за их рамки. Нельзя иметь два элемента с тем же именем в одной области; есть доступные инструменты для разрешения конфликтов имён.