Сильно! Фактически, это сообщение об ошибке означает, что 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` to `i8` --> 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 — это конструкция управления потоком выполнения программы, которая делает именно это при работе с перечислениями: она запускает разный код в зависимости от того, какой вариант перечисления имеется, и этот код может использовать данные, находящиеся внутри совпавшего варианта.
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
Управление растущими проектами спомощью пакетов, крейтов и модулейПо мере роста кодовой базы ваших программ, организация проекта будет иметь большое значение, ведь отслеживание всей программы в голове будет становиться всёболее сложным. Группируя связанные функции и разделяя код по основным функциональностям (фичам, feature), вы делаете более прозрачным понимание о том, где искать код реализующий определённую функцию и где стоит вносить изменения для того чтобы изменить её поведение.Программы, которые мы писали до сих пор, были в одном файле одного модуля. По мере роста проекта, мы можем организовывать код иначе, разделив его на несколько модулей и несколько файлов. Пакет может содержать несколько бинарных крейтов и опционально один крейт библиотеки. Пакет может включать в себя много бинарных крейтов и опционально один библиотечный крейт. По мере роста пакета вы можете извлекать части программы в отдельные крейты, которые затем станут внешними зависимостями для основного кода нашей программы. Эта глава охватывает все эти техники. В свою очередь для очень крупных проектов, состоящих из набора взаимосвязанных пакетов развивающихся вместе, Cargo предоставляет рабочие пространства, workspaces, их мы рассмотрим за пределами данной главы, в разделе "Рабочие пространства Cargo" Главы 14.Мы также обсудим инкапсуляцию деталей, которая позволяет использовать код снова на более высоком уровне: единожды реализовав какую-то операцию, другой код может вызывать этот код через публичный интерфейс, не зная как работает реализация. То, как вы пишете код, определяет какие части общедоступны для использования другим кодом и какие части являются закрытыми деталями реализации для которых вы оставляете право на изменения только за собой. Это ещё один способ ограничить количество деталей, которые вы должны держать в голове.Связанное понятие - это область видимости: вложенный контекст в котором написан код имеющий набор имён, которые определены «в текущей области видимости». При чтении, письме и компиляции кода, программистам и компиляторам необходимо знать,относится ли конкретное имя в определённом месте к переменной, к функции, к структуре, к перечислению, к модулю, к константе или другому элементу и что означает этот элемент. Можно создавать области видимости и изменять какие имена входят или выходят за их рамки. Нельзя иметь два элемента с тем же именем в одной области; есть доступные инструменты для разрешения конфликтов имён.
Rust имеет ряд функций, которые позволяют управлять организацией кода, в том числе управлять тем какие детали открыты, какие детали являются частными, какие имена есть в каждой области вашей программы. Эти функции иногда вместе именуемые модульнойсистемой включают в себя:
Пакеты: Функционал Cargo позволяющий собирать, тестировать и делиться крейтами Крейты: Дерево модулей, которое создаёт библиотечный или исполняемый файл Модули и use: Позволяют вместе контролировать организацию, область видимости и скрытие путей Пути: способ именования элемента, такого как структура, функция или модуль В этой главе мы рассмотрим все эти функции, обсудим как они взаимодействуют и объясним, как использовать их для управления областью видимости. К концу у вас должно появиться солидное понимание модульной системы и умение работать с областями видимости на уровне профессионала!
Пакеты и крейты Первые части модульной системы, которые мы рассмотрим — это пакеты и крейты. Крейт — это наименьший объем кода, который компилятор Rust рассматривает за раз. Даже если вы запустите rustc вместо cargo и передадите один файл с исходным кодом (как мы уже делали в разделе «Написание и запуск программы на Rust» Главы 1), компилятор считает этот файл крейтом. Крейты могут содержать модули, и модули могут быть определены в других файлах, которые компилируются вместе с крейтом, как мы увидим в следующих разделах. Крейт может быть одним из двух видов: бинарный крейт или библиотечный крейт. Бинарные крейты — это программы, которые вы можете скомпилировать в исполняемые файлы, которые вы можете запускать, например программу командной строки или сервер. У каждого бинарного крейта должна быть функция с именем main , которая определяет, что происходит при запуске исполняемого файла. Все крейты, которые мы создали до сих пор, были бинарными крейтами. Библиотечные крейты не имеют функции main и не компилируются в исполняемый файл. Вместо этого они определяют функциональность, предназначенную для совместного использования другими проектами. Например, крейт rand , который мы использовали в Главе 2 обеспечивает функциональность, которая генерирует случайные числа. В большинстве случаев, когда Rustaceans говорят «крейт», они имеют в виду библиотечный крейт, и они используют «крейт» взаимозаменяемо с общей концепцией программирования «библиотека». Корневой модуль крейта — это исходный файл, из которого компилятор Rust начинает собирать корневой модуль вашего крейта (мы подробно объясним модули в разделе «Определение модулей для контроля видимости и закрытости» ). Пакет — это набор из одного или нескольких крейтов, предоставляющий набор функциональности. Пакет содержит файл Cargo.toml, в котором описывается, как собирать эти крейты. На самом деле Cargo — это пакет, содержащий бинарный крейт для инструмента командной строки, который вы использовали для создания своего кода. Пакет Cargo также содержит библиотечный крейт, от которого зависит бинарный крейт. Другие проекты тоже могут зависеть от библиотечного крейта Cargo, чтобы использовать ту же логику, что и инструмент командной строки Cargo. Пакет может содержать сколько угодно бинарных крейтов, но не более одного библиотечного крейта. Пакет должен содержать хотя бы один крейт, библиотечный или бинарный. Давайте пройдёмся по тому, что происходит, когда мы создаём пакет. Сначала введём команду cargo new :