В нашей последовательной серии материалов мы рассмотрим базовые основы новомодного языка Rust. А во второй части цикла на основе изученного попробуем написать самые простые смарт-контракты для таких блокчейн-проектов, как Solana. В этом туториале будет много примеров, мало теории и быстрый темп продвижения.
Этот пост — вольный перевод на русский вот этой оригинальной статьи (с нашими дополнениями в местах, где это показалось нужным), которую написал Стив Донован. Начало можно найти вот здесь, а оглавление всей серии — вот тут.
Enums
— это типы, которые имеют несколько определенных значений. Например, Direction
из примера ниже имеет только четыре возможных значения:
enum Direction { Up, Down, Left, Right } ... // `start` is type `Direction` let start = Direction::Left;
Они могут иметь методы, определенные для них, как и структуры.
Выражение match
— это основной способ работы со значениями перечислений. Вот типичный пример:
impl Direction { fn as_str(&self) -> &'static str { match *self { // *self has type Direction Direction::Up => "Up", Direction::Down => "Down", Direction::Left => "Left", Direction::Right => "Right" } } }
Пунктуация имеет значение. Обратите внимание на *
перед self
. Это легко забыть, потому что часто Rust предполагает это по умолчанию (мы пишем self.first_name
, а не (*self).first_name)
. Однако сопоставление — более надежный подход.
Отсутствие этого подхода приведет к целому ряду сообщений об ошибках, которые сводятся к следующему несоответствию типов:
= note: expected type `&Direction` = note: found type `Direction`
Это происходит потому, что self
имеет тип &Direction
. Поэтому мы должны добавить *
для этого типа.
Как и структуры, перечисления также могут реализовывать интересные фичи, и наш старый друг #[derive(Debug)]
может быть тоже добавлен к Direction
:
println!("start {:?}",start); // start Left
Так что метод as_str
на самом деле не нужен, поскольку мы всегда можем получить имя напрямую из Debug
.
Здесь не следует предполагать какого-либо определенного упорядочивания — нет подразумеваемого целочисленного значения 'ordinal'
.
Вот метод, который определяет «преемника» каждого значения Direction
. Очень удобное использование подстановочного знака временно помещает имена перечислений в контекст метода:
fn next(&self) -> Direction { use Direction::*; match *self { Up => Right, Right => Down, Down => Left, Left => Up } } ... let mut d = start; for _ in 0..8 { println!("d {:?}", d); d = d.next(); } // d Left // d Up // d Right // d Down // d Left // d Up // d Right // d Down
Таким образом, он будет бесконечно перебирать различные направления в этом конкретном, произвольном, порядке. Это (на самом деле) очень простая машина состояний.
Значения этих перечислений нельзя сравнивать:
assert_eq!(start, Direction::Left); error[E0369]: binary operation `==` cannot be applied to type `Direction` --> enum1.rs:42:5 | 42 | assert_eq!(start, Direction::Left); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | note: an implementation of `std::cmp::PartialEq` might be missing for `Direction` --> enum1.rs:42:5
Решение состоит в том, чтобы сказать #[derive(Debug,PartialEq)]
перед enum Direction
.
Это важный момент — определяемые пользователем типы в Rust делаются с минимальным описанием. Вы наделяете их разумным поведением по умолчанию, реализуя самые общие черты. Это относится и к структурам — если вы попросите Rust вывести PartialEq
для структуры, он поступит разумно, предположив, что все поля реализуют его, и построит сравнение. Если это не так или вы хотите переопределить равенство, то можете определить PartialEq
явно.
Rust также делает перечисления в стиле C:
// enum2.rs enum Speed { Slow = 10, Medium = 20, Fast = 50 } fn main() { let s = Speed::Slow; let speed = s as u32; println!("speed {}", speed); }
Они инициализируются целым значением и могут быть преобразованы в целое число с помощью приведения типа.
Значение нужно присвоить только первому имени, в дальнейшем значение будет увеличиваться на единицу каждый раз:
enum Difficulty { Easy = 1, Medium, // is 2 Hard // is 3 }
Эти перечисления действительно имеют естественное упорядочивание, но компилятор надо попросить об этом вежливо. Если поставить #[derive(PartialEq,PartialOrd)]
перед перечислением Speed
, то действительно окажется, что Speed::Fast > Speed::Slow
и Speed::Medium != Speed::Slow
.
Но это были только основы Enums
, а сейчас покажем высший пилотаж. Перечисления в Rust в их полной форме — это как unions
в C на стероидах, как Ferrari по сравнению с Fiat Uno.
Рассмотрим проблему хранения различных значений безопасным для типов способом.
// enum3.rs #[derive(Debug)] enum Value { Number(f64), Str(String), Bool(bool) } fn main() { use Value::*; let n = Number(2.3); let s = Str("hello".to_string()); let b = Bool(true); println!("n {:?} s {:?} b {:?}", n,s,b); } // n Number(2.3) s Str("hello") b Bool(true)
Опять же, это перечисление может содержать только одно из этих значений, а его размер будет размером самого большого варианта.
Пока что это не совсем суперспособность, хотя здорово, что перечисления умеют распечатывать сами себя. Но они также знают, какое значение они содержат, и в этом суперсила match
:
fn eat_and_dump(v: Value) { use Value::*; match v { Number(n) => println!("number is {}", n), Str(s) => println!("string is '{}'", s), Bool(b) => println!("boolean is {}", b) } } .... eat_and_dump(n); eat_and_dump(s); eat_and_dump(b); //number is 2.3 //string is 'hello' //boolean is true
Вот что такое Option
и Result
– перечисления!
Нам нравится функция eat_and_dump
, но мы хотим большего — передать значение как ссылку, потому что в данный момент происходит перемещение и значение «съедается». Поэтому перепишем так:
fn dump(v: &Value) { use Value::*; match *v { // type of *v is Value Number(n) => println!("number is {}", n), Str(s) => println!("string is '{}'", s), Bool(b) => println!("boolean is {}", b) } } error[E0507]: cannot move out of borrowed content --> enum3.rs:12:11 | 12 | match *v { | ^^ cannot move out of borrowed content 13 | Number(n) => println!("number is {}",n), 14 | Str(s) => println!("string is '{}'",s), | - hint: to prevent move, use `ref s` or `ref mut s`
Есть вещи, которые нельзя делать с заимствованными ссылками. Rust не позволяет вам извлечь строку, содержащуюся в исходном значении. Он не жаловался на Number
, потому что с удовольствием копирует f64
, но String
не реализует Copy
, к сожалению.
Я уже упоминал, что match
придирчив к точным типам — далее мы следуем подсказке компилятора, и все работает. А сейчас мы просто берем ссылку на содержащуюся в ней строку, вот так:
fn dump(v: &Value) { use Value::*; match *v { Number(n) => println!("number is {}", n), Str(ref s) => println!("string is '{}'", s), Bool(b) => println!("boolean is {}", b) } } .... dump(&s); // string is 'hello'
Прежде чем двигаться дальше, с эйфорией от успешной компиляции со стороны Rust, давайте сделаем небольшую паузу. rustc
необычайно хорош в генерации ошибок, которые имеют достаточно контекста, чтобы человек мог исправить ошибку, даже не всегда понимая ее.
Проблема заключается в сочетании точности соответствия с решимостью проверяющего пресечь любую попытку нарушить правила. Одно из этих правил гласит, что нельзя выдергивать значение, которое принадлежит какому-то собственному типу.
Некоторое знание C++ здесь не помешает, поскольку C++ всегда бездумно скопирует свой способ решения проблемы, независимо от того, имеет ли эта копия смысл. Вы получите точно такую же ошибку, если попытаетесь вытащить строку из вектора, скажем, с помощью *v.get(0).unwrap()
(*
— потому что индексирование возвращает ссылки). Иногда клон — не такое уж плохое решение.
Что касается соответствия, то можно рассматривать Str(s) =>
как сокращение от Str(s: String) =>
. Создается локальная переменная (часто называемая привязкой), в большинстве случаев этот инферентный тип — это круто, когда вы «съедаете» значение и извлекаете его содержимое. Но здесь нам действительно нужно s: &String
, а ссылка — это подсказка, которая гарантирует: мы просто хотим взять эту строку.
Здесь мы действительно хотим извлечь эту строку, и нас не волнует последующее значение перечисления. Как обычно в таком случае, _
будет соответствовать чему угодно.
impl Value { fn to_str(self) -> Option<String> { match self { Value::Str(s) => Some(s), _ => None } } } ... println!("s? {:?}", s.to_str()); // s? Some("hello") // println!("{:?}", s) // error! s has moved...
Именование имеет значение — это называется to_str
, а не as_str
. Можно написать метод, который просто заимствует эту строку как Option<&String>
(ссылка будет иметь то же время жизни, что и значение перечисления), но вы не будете называть его to_str
.
Подводя итог, можно переписать to_str
вот так — и это полностью эквивалентно:
fn to_str(self) -> Option<String> { if let Value::Str(s) = self { Some(s) } else { None } }
Продолжение следует…
Прокси (proxy), или прокси-сервер — это программа-посредник, которая обеспечивает соединение между пользователем и интернет-ресурсом. Принцип…
Согласитесь, было бы неплохо соединить в одно сайт и приложение для смартфона. Если вы еще…
Повсеместное распространение смартфонов привело к огромному спросу на мобильные игры и приложения. Миллиарды пользователей гаджетов…
В перечне популярных чат-ботов с искусственным интеллектом Google Bard (Gemini) еще не пользуется такой популярностью…
Скрипт (англ. — сценарий), — это небольшая программа, как правило, для веб-интерфейса, выполняющая определенную задачу.…
Дедлайн (от англ. deadline — «крайний срок») — это конечная дата стачи проекта или задачи…