Определение перечисления

Рассмотрим ситуацию, когда использование перечисления весьма желательно.

Мы можем создать определение перечисления IpAddrKind:


# #![allow(unused_variables)]
#fn main() {
enum IpAddrKind {
    V4,
    V6,
}
#}

Enum Values

Экземпляр перечисления можно создать следующим образом:

enum IpAddrKind {
    V4,
    V6,
}
fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;
}

Обратите внимание, что перед значением стоит идентификатор. :: разделяет эти данные. Две эти величины одного типа IpAddrKind. Следовательно, мы можем использовать этот тип при указании типа данных функции:


# #![allow(unused_variables)]
#fn main() {
 enum IpAddrKind {
     V4,
     V6,
 }

fn route(ip_type: IpAddrKind) { }
#}

Значениями данной функции будут значения перечисления данного типа:


# #![allow(unused_variables)]
#fn main() {
 enum IpAddrKind {
     V4,
     V6,
 }

 fn route(ip_type: IpAddrKind) { }

route(IpAddrKind::V4);
route(IpAddrKind::V6);
#}

Преимущества использования перечислений. Мы можем получать информацию о типе данных, а не сами данные. Пример 6-1:


# #![allow(unused_variables)]
#fn main() {
enum IpAddrKind {
    V4,
    V6,
}

struct IpAddr {
    kind: IpAddrKind,
    address: String,
}

let home = IpAddr {
    kind: IpAddrKind::V4,
    address: String::from("127.0.0.1"),
};

let loopback = IpAddr {
    kind: IpAddrKind::V6,
    address: String::from("::1"),
};
#}

Пример 6-1: Сохранение данных IpAddrKindв структуре struct

Мы также можем реализовать перечисление содержащие переменные данные внутри элементов:


# #![allow(unused_variables)]
#fn main() {
enum IpAddr {
    V4(String),
    V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1"));

let loopback = IpAddr::V6(String::from("::1"));
#}

Мы добавили данные в каждый вариант перечисления. Таким образом, мы упростили наш предыдущий код и получили тот же результат.

Ещё одно преимущество перечисления заключается в том, что каждый вариант данных может иметь различный тип данных:


# #![allow(unused_variables)]
#fn main() {
enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);

let loopback = IpAddr::V6(String::from("::1"));
#}

Мы показали несколько различных вариантов, которые можно использовать для определения IP-адреса.

Рассмотрим какое решение реализовано в стандартной библиотеке:


# #![allow(unused_variables)]
#fn main() {
struct Ipv4Addr {
    // details elided
}

struct Ipv6Addr {
    // details elided
}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}
#}

Этот код иллюстрирует возможность добавления любого типа данных в значение перечисления: строк, чисел, структур и пр. Вы даже можете включит другие перечисления! Стандартные типы данных, не очень сложны, хотя, потенциально, могут быть очень сложными (вложенность данных может быть очень глубокой).

Обратите внимание, что имея определение перечисления IpAddr в стандартной библиотеке, вы можете создавать и использовать свою собственную реализацию без каких-либо конфликтов, т.к. вы можете не добавлять определение стандартной библиотеки в область видимости. Подробнее об этом поговорим в главе 7.

Рассмотрим другой пример 6-2: в этом примере каждый элемент перечисления имеете свой, особый, тип данных внутри:


# #![allow(unused_variables)]
#fn main() {
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}
#}

Пример 6-2: Перечисление Message, в котором каждый элемент хранит различные данные (наиболее удобные и нужные для использования)

Это перечисление имеет 4 элемента:

  • Quit - пустой элемент.
  • Move - имеющий анонимную структуру.
  • Write имеет строку String.
  • ChangeColor имеет кортеж i32 значений.

Это определение компактно хранит данные. Оно подобно определению множеству структур:


# #![allow(unused_variables)]
#fn main() {
struct QuitMessage; // unit struct
struct MoveMessage {
    x: i32,
    y: i32,
}
struct WriteMessage(String); // tuple struct
struct ChangeColorMessage(i32, i32, i32); // tuple struct
#}

Но когда мы используем различные структуры, которые имеют свои собственные типы, мы не можем легко определять функции, которые могли бы любые типы сообщений, которые может содержать в себе перечисление.

Так и структуры и перечисления - это всё группировочные типы данные семантика добавления функций в них идентична. Мы используем структуру impl:

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

impl Message {
    fn call(&self) {
        // method body would be defined here
    }
}
fn main() {
    let m = Message::Write(String::from("hello"));
    m.call();
}

Метод может использовать self для получения значения. В данном случае, self - это строка..

Теперь рассмотрим наиболее часто использованное перечисление стандартной библиотеки Option.

Перечисление Option и его преимущества перед нулевыми значениями

В предудущей секции, вы рассматривали как перечисление IpAddr позволяет нам использовать систему типов Rust для архивирования информации. Перечисление Option используется так часто, потому что оно реализует часто используемый сценарий, в котором значение может быть чем-то или ничем. Этот шаблон позволяет уберечь программу от множества ошибок.

Дизайн языка программирования спроектирован так, что не имеет нулевых значений. Перечисление Option<T> даёт возможность показать, что значение нулевое с помощью одного из своих элементов defined by the standard library:


# #![allow(unused_variables)]
#fn main() {
enum Option<T> {
    Some(T),
    None,
}
#}

Перечисление Option<T> очень важно для всей стандартной библиотеки. Также вы можете использовать коде элементы перечисления Option<T> h Option:: без префикса:Some, None.

О подстановочных типах <T> (дженерика), их синтаксисе мы ещё не говорили Поговорим об этом в главе 10. <T> это обозначения того, что перечисление может иметь любой тип. Пример:


# #![allow(unused_variables)]
#fn main() {
let some_number = Some(5);
let some_string = Some("a string");

let absent_number: Option<i32> = None;
#}

Если мы используем значение None, то нам обязательно нужно указывать конкретный тип данных, который мы будем использовать.

Если вы используете значение Some, то это значит, что какое-либо значение существует. Когда вы используете значение None, это значит, что никакого значения нет.

Т.к. Option<T> и T (T - это любой тип данных) разные типы данных, компилятор не позволяет использовать значение Option<T> вместо конкретного типа данных. Этот код не будет скомпилирован, т.к. тут происходит попытка суммирования двух разных типов данных i8 и Option<i8>:

let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y;

Оприсание ошибки:

error[E0277]: the trait bound `i8: std::ops::Add<std::option::Option<i8>>` is
not satisfied
 -->
  |
7 | let sum = x + y;
  |           ^^^^^
  |

Для того, чтобы испльзовать содержащееся значение в в Some необходимо конвертировать его в данные. Это также типичное действие помогающее избежать ошибок.

Если есть такая возможность, что значение может иметь нулевое значение, лучше всего использовать тип данных Option<T>. Использование этого перечисления значительно повышает уровень безопасности кода.

Вам надо хорошо разобраться с методами перечисления Option<T>. Это поможет вам лучше понимать исходный код Rust стандартной библиотеки.

В следующей секции будет рассмотрена конструкция match. Это управляющая конструкция, которая используется совместно с перечислениями. Результат выполнения той или иной ветви кода зависит от значения перечисления.