Перечисления (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.2720.00Shape::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", а ещё придётся вручную проверять, какие поля на самом деле нужны для каждого типа. Перечисление делает неправильные сочетания невозможными с самого начала, а компилятор заставляет обработать каждый вариант.
Практика
Заголовок раздела «Практика»1. Считаем монеты
Заголовок раздела «1. Считаем монеты»Создай перечисление 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 центов2. Добавляем треугольник
Заголовок раздела «2. Добавляем треугольник»Возьми перечисление 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.2818.0012.00Попробуй удалить из match ветку Shape::Triangle. Rust откажется компилировать код, потому что match больше не покрывает все варианты. Так компилятор следит, чтобы ты никогда не забыл ни одного случая.
3. Температуры
Заголовок раздела «3. Температуры»Создай перечисление 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 °C100.0 °C10.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
Что почитать
Заголовок раздела «Что почитать»- Определение Enum — книга «Язык программирования Rust»: варианты с данными, методы у перечислений и почему в Rust есть
Optionвместо null (перевод на русский). - Конструкция потока управления match — книга «Язык программирования Rust»: подробно о
matchпо перечислениям, с примером сортировки монет и шаблонами «на всё остальное» (перевод на русский). - Перечисления — Rust на примерах: запускаемые примеры, в том числе варианты с именованными полями вроде
Click { x: i64, y: i64 }(перевод на русский). - Enumerations — The Rust Reference (на английском): все правила, например как задать вариантам явные числовые значения.
Option— документация стандартной библиотеки (на английском): настоящее определение перечисления, которым ты будешь пользоваться чаще всего, и все его вспомогательные методы.