Перечисления в Rust
Что вы узнаете: перечисления Rust как размеченные объединения (tagged unions, сделанные правильно),
matchдля исчерпывающего сопоставления с образцом и то, как перечисления заменяют иерархии классов C++ и размеченные объединения C с безопасностью, которую обеспечивает компилятор.
- Перечисления в Rust — это размеченные объединения, то есть суммовые типы из нескольких возможных различных типов с меткой, которая определяет конкретный вариант
- Для разработчиков на C: перечисления в Rust могут содержать данные (размеченные объединения, сделанные правильно — компилятор отслеживает, какой вариант активен)
- Для разработчиков на C++: перечисления Rust похожи на
std::variant, но с исчерпывающим сопоставлением с образцом, без исключенийstd::getи без шаблонного кодаstd::visit - Размер
enumравен размеру самого большого возможного варианта. Отдельные варианты не связаны друг с другом и могут иметь совершенно разные типы - Типы
enum— одна из самых мощных возможностей языка: они заменяют целые иерархии классов в C++ (подробнее об этом в разборах кейсов)
fn main() {
enum Numbers {
Zero,
SmallNumber(u8),
BiggerNumber(u32),
EvenBiggerNumber(u64),
}
let a = Numbers::Zero;
let b = Numbers::SmallNumber(42);
let c : Numbers = a; // Ок — тип a это Numbers
let d : Numbers = b; // Ок — тип b это Numbers
}
Оператор match в Rust
matchв Rust — это аналог C-шного “switch”, но на стероидахmatchможно использовать для сопоставления с образцом простых типов данных,struct,enum- Оператор
matchдолжен быть исчерпывающим, то есть покрывать все возможные случаи для данноготипа. Символ_используется как подстановочный шаблон для случая «все остальные» matchможет возвращать значение, но все ветки (=>) должны возвращать значение одного и того же типа
fn main() {
let x = 42;
// В этом случае _ покрывает все числа, кроме явно перечисленных
let is_secret_of_life = match x {
42 => true, // Возвращаемый тип — булево значение
_ => false, // Возвращаемый тип — булево значение
// Это не скомпилируется, потому что возвращаемый тип не булев
// _ => 0
};
println!("{is_secret_of_life}");
}
Оператор match в Rust
matchподдерживает диапазоны, булевы фильтры и охранные условияif
fn main() {
let x = 42;
match x {
// Обратите внимание: =41 обеспечивает включение верхней границы диапазона
0..=41 => println!("Less than the secret of life"),
42 => println!("Secret of life"),
_ => println!("More than the secret of life"),
}
let y = 100;
match y {
100 if x == 43 => println!("y is 100% not secret of life"),
100 if x == 42 => println!("y is 100% secret of life"),
_ => (), // Ничего не делать
}
}
Оператор match в Rust
matchиenumчасто используются вместе- Оператор match может «связать» содержащееся значение с переменной. Используйте
_, если значение не важно - Макрос
matches!можно использовать для проверки на конкретный вариант
- Оператор match может «связать» содержащееся значение с переменной. Используйте
fn main() {
enum Numbers {
Zero,
SmallNumber(u8),
BiggerNumber(u32),
EvenBiggerNumber(u64),
}
let b = Numbers::SmallNumber(42);
match b {
Numbers::Zero => println!("Zero"),
Numbers::SmallNumber(value) => println!("Small number {value}"),
Numbers::BiggerNumber(_) | Numbers::EvenBiggerNumber(_) => println!("Some BiggerNumber or EvenBiggerNumber"),
}
// Булева проверка на конкретные варианты
if matches!(b, Numbers::Zero | Numbers::SmallNumber(_)) {
println!("Matched Zero or small number");
}
}
Оператор match в Rust
matchтакже может выполнять сопоставление с помощью деструктуризации и срезов
fn main() {
struct Foo {
x: (u32, bool),
y: u32
}
let f = Foo {x: (42, true), y: 100};
match f {
// Захватываем значение x в переменную tuple
Foo{y: 100, x : tuple} => println!("Matched x: {tuple:?}"),
_ => ()
}
let a = [40, 41, 42];
match a {
// Последний элемент среза должен быть 42. @ используется для связывания с образцом
[rest @ .., 42] => println!("{rest:?}"),
// Первый элемент среза должен быть 42. @ используется для связывания с образцом
[42, rest @ ..] => println!("{rest:?}"),
_ => (),
}
}
Упражнение: сложение и вычитание с помощью match и enum
🟢 Начальный уровень
- Напишите функцию, которая реализует арифметические операции над беззнаковыми 64-битными числами
- Шаг 1: определите перечисление для операций:
#![allow(unused)]
fn main() {
enum Operation {
Add(u64, u64),
Subtract(u64, u64),
}
}
- Шаг 2: определите перечисление результата:
#![allow(unused)]
fn main() {
enum CalcResult {
Ok(u64), // Успешный результат
Invalid(String), // Сообщение об ошибке для недопустимых операций
}
}
- Шаг 3: реализуйте
calculate(op: Operation) -> CalcResult- Для Add: вернуть Ok(sum)
- Для Subtract: вернуть Ok(difference), если первое число >= второго, иначе Invalid(“Underflow”)
- Подсказка: используйте сопоставление с образцом в функции:
#![allow(unused)]
fn main() {
match op {
Operation::Add(a, b) => { /* ваш код */ },
Operation::Subtract(a, b) => { /* ваш код */ },
}
}
Решение (нажмите, чтобы развернуть)
enum Operation {
Add(u64, u64),
Subtract(u64, u64),
}
enum CalcResult {
Ok(u64),
Invalid(String),
}
fn calculate(op: Operation) -> CalcResult {
match op {
Operation::Add(a, b) => CalcResult::Ok(a + b),
Operation::Subtract(a, b) => {
if a >= b {
CalcResult::Ok(a - b)
} else {
CalcResult::Invalid("Underflow".to_string())
}
}
}
}
fn main() {
match calculate(Operation::Add(10, 20)) {
CalcResult::Ok(result) => println!("10 + 20 = {result}"),
CalcResult::Invalid(msg) => println!("Error: {msg}"),
}
match calculate(Operation::Subtract(5, 10)) {
CalcResult::Ok(result) => println!("5 - 10 = {result}"),
CalcResult::Invalid(msg) => println!("Error: {msg}"),
}
}
// Вывод:
// 10 + 20 = 30
// Error: Underflow
Ассоциированные методы в Rust
implможет определять методы, ассоциированные с типами вродеstruct,enumи т. д.- Методы могут необязательно принимать
selfкак параметр.selfконцептуально похож на передачу указателя на структуру первым параметром в C или наthisв C++ - Ссылка на
selfможет быть неизменяемой (по умолчанию:&self), изменяемой (&mut self) илиself(передача владения) - Ключевое слово
Selfможно использовать как сокращение, подразумевающее тип
- Методы могут необязательно принимать
struct Point {x: u32, y: u32}
impl Point {
fn new(x: u32, y: u32) -> Self {
Point {x, y}
}
fn increment_x(&mut self) {
self.x += 1;
}
}
fn main() {
let mut p = Point::new(10, 20);
p.increment_x();
}
Упражнение: сложение и преобразование точки
🟡 Средний уровень — требует понимания разницы между перемещением и заимствованием по сигнатурам методов
- Реализуйте следующие ассоциированные методы для
Pointadd()принимает другуюPointи увеличивает значения x и y на месте (подсказка: используйте&mut self)transform()поглощает существующуюPoint(подсказка: используйтеself) и возвращает новуюPoint, возводя x и y в квадрат
Решение (нажмите, чтобы развернуть)
struct Point { x: u32, y: u32 }
impl Point {
fn new(x: u32, y: u32) -> Self {
Point { x, y }
}
fn add(&mut self, other: &Point) {
self.x += other.x;
self.y += other.y;
}
fn transform(self) -> Point {
Point { x: self.x * self.x, y: self.y * self.y }
}
}
fn main() {
let mut p1 = Point::new(2, 3);
let p2 = Point::new(10, 20);
p1.add(&p2);
println!("After add: x={}, y={}", p1.x, p1.y); // x=12, y=23
let p3 = p1.transform();
println!("After transform: x={}, y={}", p3.x, p3.y); // x=144, y=529
// p1 больше недоступен — transform() его поглотил
}