Основы Rust: подробно про Enums

Игорь Грегорченко

В нашей последовательной серии материалов мы рассмотрим базовые основы новомодного языка Rust. А во второй части цикла на основе изученного попробуем написать самые простые смарт-контракты для таких блокчейн-проектов, как Solana. В этом туториале будет много примеров, мало теории и быстрый темп продвижения.

Этот пост — вольный перевод на русский вот этой оригинальной статьи (с нашими дополнениями в местах, где это показалось нужным), которую написал Стив Донован. Начало можно найти вот здесь, а оглавление всей серии — вот тут.

Простые перечисления (Enums)

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
    }
}

 

Продолжение следует…

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023