В нашей последовательной серии материалов мы рассмотрим базовые основы новомодного языка 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
}
}
Продолжение следует…
На фоне роста спроса на ликвидность в бычьем рынке 2025 года, криптозаймы снова выходят на…
Прокси (proxy), или прокси-сервер — это программа-посредник, которая обеспечивает соединение между пользователем и интернет-ресурсом. Принцип…
Согласитесь, было бы неплохо соединить в одно сайт и приложение для смартфона. Если вы еще…
Повсеместное распространение смартфонов привело к огромному спросу на мобильные игры и приложения. Миллиарды пользователей гаджетов…
В перечне популярных чат-ботов с искусственным интеллектом Google Bard (Gemini) еще не пользуется такой популярностью…
Скрипт (англ. — сценарий), — это небольшая программа, как правило, для веб-интерфейса, выполняющая определенную задачу.…