Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Перечисления

Перечисления, или просто “энамы” (enums), в Rust могут иметь две формы:

  • Просто перечисление значений, как в C, C++, Java и т.д.
  • Контейнер типов

Рассмотрим каждую из этих форм.

Перечисление как в C

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

#![allow(unused)]
fn main() {
enum EnumName {
    Элемент1,
    Элемент2,
    …,
    ЭлементN
}
}

Например:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let ip_v4 = IpAddrKind::V4;
    let ip_v6 = IpAddrKind::V6;
}

Так же, как и в Си, с элементами перечисления можно ассоциировать число. Далее это число можно получить путём приведения значения типа энама к usize:

enum HttpStatus {
    Ok = 200,
    NotModified = 304,
    NotFound = 404,
}

fn main() {
    println!("{}", HttpStatus::Ok as usize); // 200
}

Перечисление как объединение типов

В отличие от C, в Rust перечисление может включать в себя не только значения, но и различные типы (структуры и кортежи). При этом объект перечисления будет принадлежать одному из этих внутренних типов. То есть, объявляя enum, мы объявляем не перечень возможных значений, а перечень типов, к одному из которых должен принадлежать объект энама.

Например, IPv4 адрес кодируется 4 байтами, а IPv6-адрес — 20 байтами. При этом IPv4-адрес, как правило, записывают при помощи 4 чисел в диапазоне 0 - 255, а IPv6-адрес обычно записывают строкой. Мы можем сделать перечисление, значение которого будет представлено либо кортежем из 4 байт, либо строкой:

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

fn main() {
    let home = IpAddr::V4(127, 0, 0, 1);
    let loopback = IpAddr::V6(String::from("::1"));
}

Теперь рассмотрим пример перечисления со структурами. Напишем перечисление “фигура”, которое состоит из двух типов: квадрат и прямоугольник.

enum Shape {
    Square { width: f32 },
    Rectangle { width: f32, height: f32 }
}
fn main() {
    let square = Shape::Square { width: 4.0 };
}

Одним из преимуществ использования энамов является то, что ими удобно пользоваться в match операторе, так как компилятор заставит нас проверить все варианты перечисления.

enum Shape {
    Square { width: f32 },
    Rectangle { width: f32, height: f32 }
}

fn calc_area(shape: &Shape) -> f32 {
    match shape { // Нужно проверить и Square, и Rectangle
        Shape::Square { width } => width * width,
        Shape::Rectangle { width, height } => width * height,
    }
}

fn main() {
    let square = Shape::Square { width: 4.0 };
    println!("{}", calc_area(&square));
}

Для перечислений можно добавлять методы точно так же, как и для структур:

enum Shape {
    Square { width: f32 },
    Rectangle { width: f32, height: f32 }
}

impl Shape {
    fn calc_area(&self) -> f32 {
        match self {
            Shape::Square { width } => width * width,
            Shape::Rectangle { width, height } => width * height,
        }
    }
}

fn main() {
    let square = Shape::Square { width: 4.0 };
    println!("{}", square.calc_area());
}

Note

Перечисления в Rust основаны на, так называемых, ADT (Algebraic data type — алгебраические типы данных). Это раздел теории, которая рассматривает составные типы данных как комбинации объединений и пересечений других типов.

if-let

Как мы уже сказали, оператор match заставит нас перебрать все возможные варианты перечисления. Однако если мы заинтересованы только в одном варианте, то мы можем использовать конструкцию if-let — версия оператора if с деструктурирующим шаблоном.

if let шаблон = объект {
    // здесь работаем с переменными из деструктурирующего шаблона
}

Например, при помощи if-let проверим, является ли объект типа Shape квадратом:

enum Shape {
    Square { width: f32 },
    Rectangle { width: f32, height: f32 }
}
fn main() {
    let s = Shape::Square { width: 4.0 };
    if let Shape::Square { width } = s {
        println!("This is square of width {width}");
    }
}

Разумеется, у if-let, как и у обычного if, может быть else ветка.

Лэйаут в памяти

При помощи энамов мы фактически можем хранить значения разных типов в массиве.

enum MyEnum {
    Byte(u8),
    UInt(u32),
}

fn main() {
    let arr = [MyEnum::Byte(1), MyEnum::UInt(5)];
}

Это достигается за счёт того, что под элементы энама резервируется такое количество памяти, которое необходимо для записи наибольшего из возможных вариантов перечисления. То есть за гибкость приходится платить потенциальным перерасходом памяти.

Также, кроме непосредственно самого значения, каждый объект энама содержит дискриминатор — число (может быть размером от u8 до u32), которое позволяет узнать, какой именно из вариантов перечисления хранится в данном объекте.

Массив из примера выше выглядит в памяти примерно так:

┏━━━━━━━━━━━━━┯━━━━━━━━━━━┯━━━━━━━━━━━━┳━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━┓
┃дискриминатор┆u8 значение┆пустое место┃дискриминатор┆     u32 значение       ┃
┗━━━━━━━━━━━━━┷━━━━━━━━━━━┷━━━━━━━━━━━━┻━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━┛

Именно дискриминатор проверяют операторы match и let-if, когда проверяют, к какому внутреннему типу относится объект перечисления.

Еще раз про перечисление “как в C”

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

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

С семантической точки зрения V4 и V6 — это никакие не значения, а просто синглтон структуры. Соответственно, в памяти объекты таких перечислений, представлены только дискриминатором.

Более того, когда мы присваиваем элементам перечисления некое числовое значение, то мы просто задаём конкретные значения для дискриминатора.

#![allow(unused)]
fn main() {
enum HttpStatus {
    Ok = 200,          // дискриминатор = 200
    NotModified = 304, // дискриминатор = 304
    NotFound = 404,    // дискриминатор = 404
}
}