Перейти к содержимому

Перечисления (enum) в Rust

Перечисление (enum, от enumeration) задаёт тип, значение которого — один вариант из фиксированного набора. Если структура говорит «у этой штуки есть все эти поля», то перечисление говорит «эта штука — ровно один из этих вариантов».

enum Direction {
North,
South,
East,
West,
}
fn main() {
let heading = Direction::North;
match heading {
Direction::North => println!("Идём на север"),
Direction::South => println!("Идём на юг"),
Direction::East => println!("Идём на восток"),
Direction::West => println!("Идём на запад"),
}
}

Вывод:

Идём на север
▶ Попробуй сам

Заметь: этому match не нужна ветка _ «на всё остальное». Все возможные значения Direction перечислены, и Rust сам видит, что match полный.

Здесь перечисления в Rust идут дальше, чем обычные enum в других языках: каждый вариант может нести свои данные:

enum Shape {
Circle(f64), // радиус
Rectangle(f64, f64), // ширина, высота
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(radius) => 3.14159 * radius * radius,
Shape::Rectangle(width, height) => width * height,
}
}
fn main() {
let circle = Shape::Circle(3.0);
let rectangle = Shape::Rectangle(4.0, 5.0);
println!("{:.2}", area(&circle));
println!("{:.2}", area(&rectangle));
}

Вывод:

28.27
20.00
▶ Попробуй сам

Shape::Circle несёт одно f64 (радиус), а Shape::Rectangle — два. match сам достаёт эти данные в каждой ветке, и отдельно «распаковывать» их не нужно.

Как и у структуры, у перечисления может быть блок impl с методами. Внутри метода обычно пишут match self, чтобы решить, что делать для каждого варианта:

#[derive(Debug)]
enum Light {
Red,
Yellow,
Green,
}
impl Light {
fn seconds(&self) -> u32 {
match self {
Light::Red => 30,
Light::Yellow => 3,
Light::Green => 25,
}
}
fn next(&self) -> Light {
match self {
Light::Red => Light::Green,
Light::Green => Light::Yellow,
Light::Yellow => Light::Red,
}
}
}
fn main() {
let mut light = Light::Red;
for _ in 0..4 {
println!("{:?} горит {} с", light, light.seconds());
light = light.next();
}
}

Вывод:

Red горит 30 с
Green горит 25 с
Yellow горит 3 с
Red горит 30 с
▶ Попробуй сам

next возвращает совершенно новый Light, а light = light.next(); заменяет им старый. Поэтому light объявлена с mut. #[derive(Debug)] работает с перечислениями так же, как со структурами, и {:?} выводит имя варианта (поэтому в выводе английские Red, Green — это имена из кода).

Логика в методах живёт рядом с типом. Кто угодно может вызвать light.seconds() и не знать, как выбирается время. А если позже добавить новый вариант, например FlashingYellow, компилятор покажет каждый match, который нужно обновить.

Option — перечисление, которым ты уже пользуешься

Заголовок раздела «Option — перечисление, которым ты уже пользуешься»

Тип Option, с которым ты как следует познакомишься на отдельной странице, — это просто перечисление из стандартной библиотеки:

enum Option<T> {
Some(T),
None,
}

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

Почему бы не взять структуру с полем «тип»?

Заголовок раздела «Почему бы не взять структуру с полем «тип»?»

Можно представить себе подделку: структура со строковым полем вроде "circle" или "rectangle". Но тогда ничто не мешает случайно написать "circl", а ещё придётся вручную проверять, какие поля на самом деле нужны для каждого типа. Перечисление делает неправильные сочетания невозможными с самого начала, а компилятор заставляет обработать каждый вариант.

Создай перечисление Coin с вариантами Penny, Nickel, Dime и Quarter (американские монеты в 1, 5, 10 и 25 центов). Напиши fn value_in_cents(coin: &Coin) -> u32 и сложи монеты в кошельке: четвертак, дайм, никель, пенни и ещё один четвертак. Подсказка: положи монеты в массив и перебери &purse в цикле.

Показать решение
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: &Coin) -> u32 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() {
let purse = [Coin::Quarter, Coin::Dime, Coin::Nickel, Coin::Penny, Coin::Quarter];
let mut total = 0;
for coin in &purse {
total += value_in_cents(coin);
}
println!("Итого: {total} центов");
}

Вывод:

Итого: 66 центов
▶ Попробуй сам

Возьми перечисление Shape с этой страницы и добавь вариант Triangle, который хранит длины трёх сторон. Затем напиши функцию perimeter (длина всей границы фигуры), которая работает для любой фигуры. Подсказка: периметр круга — 2.0 * 3.14159 * radius, а периметр треугольника — сумма трёх сторон.

Показать решение
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Triangle(f64, f64, f64),
}
fn perimeter(shape: &Shape) -> f64 {
match shape {
Shape::Circle(radius) => 2.0 * 3.14159 * radius,
Shape::Rectangle(width, height) => 2.0 * (width + height),
Shape::Triangle(a, b, c) => a + b + c,
}
}
fn main() {
let shapes = [
Shape::Circle(1.0),
Shape::Rectangle(4.0, 5.0),
Shape::Triangle(3.0, 4.0, 5.0),
];
for shape in &shapes {
println!("{:.2}", perimeter(shape));
}
}

Вывод:

6.28
18.00
12.00
▶ Попробуй сам

Попробуй удалить из match ветку Shape::Triangle. Rust откажется компилировать код, потому что match больше не покрывает все варианты. Так компилятор следит, чтобы ты никогда не забыл ни одного случая.

Создай перечисление Temperature с двумя вариантами, каждый из которых хранит f64: Celsius и Fahrenheit. Добавь метод to_celsius(&self) -> f64 и выведи Celsius(25.0), Fahrenheit(212.0) и Fahrenheit(50.0) в °C с одним знаком после запятой. Подсказка: для перевода используй (f - 32.0) * 5.0 / 9.0.

Показать решение
enum Temperature {
Celsius(f64),
Fahrenheit(f64),
}
impl Temperature {
fn to_celsius(&self) -> f64 {
match self {
Temperature::Celsius(c) => *c,
Temperature::Fahrenheit(f) => (f - 32.0) * 5.0 / 9.0,
}
}
}
fn main() {
let readings = [
Temperature::Celsius(25.0),
Temperature::Fahrenheit(212.0),
Temperature::Fahrenheit(50.0),
];
for t in &readings {
println!("{:.1} °C", t.to_celsius());
}
}

Вывод:

25.0 °C
100.0 °C
10.0 °C
▶ Попробуй сам

Поскольку self заимствован, c и f — это ссылки (&f64). Арифметика работает со ссылками напрямую, но чтобы вернуть само значение в градусах Цельсия, пишут *c — это значит «число, на которое указывает c».

  • Перечисление задаёт тип как один из нескольких именованных вариантов: enum Direction { North, South, ... }.
  • Варианты могут нести свои данные, причём у разных вариантов данные могут быть разными.
  • match естественно разбирает данные перечисления и требует обработать каждый вариант.
  • У перечислений, как и у структур, бывают методы в блоке impl.
  • Option (и Result, о котором позже) — обычные перечисления из стандартной библиотеки, никакой магии.

Проверь себя

1. Что умеет каждый вариант перечисления в Rust, в отличие от обычных enum в других языках?

2. Почему примеру с match по Direction не нужна ветка _?

3. Чем на самом деле является Option<T> в стандартной библиотеке?

Результат: 0 / 3