Перейти к содержанию
Educora
Средний20 мин5 / 10

Структуры, методы, перечисления и match

Создавай собственные типы: структуры с полями, методы в блоке `impl`, перечисления с вариантами, их обработку через `match` и `Option` вместо `null`.

Проверь себя
В этом уроке ты узнаешь
  • Объявлять структуру, создавать её экземпляр и печатать через #[derive(Debug)]
  • Писать в блоке impl методы с &self и &mut self и ассоциированные функции
  • Создавать enum с вариантами, несущими данные, и обрабатывать его исчерпывающим match
  • Безопасно работать с возможно отсутствующим значением через Option, if let и unwrap_or

В электронном журнале у каждого ученика есть имя, класс и средний балл — хранить эти три значения в отдельных переменных неудобно, их нужно держать вместе. А светофор в каждый момент находится ровно в одном состоянии: красный, жёлтый или зелёный. Для первого случая в Rust есть структуры (struct), для второго — перечисления (enum).

Структуры

Структура — тип с именованными полями. Имя типа пишут в стиле PascalCase, а поля — в snake_case. При создании экземпляра все поля должны получить значения, а к полю обращаются через точку: aysel.score. Чтобы изменить поле, mut должен быть весь экземпляр — отдельное поле сделать mut в Rust нельзя.

Rust
#[derive(Debug)]
struct Student {
    name: String,
    grade: u8,
    score: f64,
}

fn main() {
    let mut aysel = Student {
        name: String::from("Aysel"),
        grade: 9,
        score: 91.5,
    };
    aysel.score += 2.0;
    println!("{} is in grade {}", aysel.name, aysel.grade);
    println!("{:?}", aysel);
    println!("{:#?}", aysel);
}
Ожидаемый результат
Aysel is in grade 9
Student { name: "Aysel", grade: 9, score: 93.5 }
Student {
    name: "Aysel",
    grade: 9,
    score: 93.5,
}
Атрибут #[derive(Debug)] учит компилятор печатать структуру через {:?}. А {:#?} выводит её аккуратно, каждое поле на отдельной строке.

Методы и impl

Функции, относящиеся к типу, пишут в блоке impl. Функция, первый параметр которой — self, называется методом и вызывается через точку: room.area(). Функция без параметра self — ассоциированная функция, её вызывают через ::: Rectangle::new(4, 3) — именно такая и String::from. Внутри impl запись Self заменяет имя самого типа.

Rust
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn new(width: u32, height: u32) -> Self {
        Self { width, height }
    }

    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn scale(&mut self, factor: u32) {
        self.width *= factor;
        self.height *= factor;
    }
}

fn main() {
    let mut room = Rectangle::new(4, 3);
    println!("Area: {}", room.area());
    room.scale(2);
    println!("Scaled: {}x{}, area {}", room.width, room.height, room.area());
}
Ожидаемый результат
Area: 12
Scaled: 8x6, area 48
Первый параметрЗначениеВызов
&selfтолько читает экземплярroom.area()
&mut selfизменяет экземплярroom.scale(2)
selfзабирает владение, после вызова экземпляр недоступенtext.into_bytes()
нет selfассоциированная функция, часто конструкторRectangle::new(4, 3)

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

enum говорит, что значение — один из нескольких вариантов. В Rust варианты могут нести собственные данные: ничего, именованные поля или значения в скобках. Перечисления обрабатывают через match: каждая ветвь имеет вид образец => выражение. match должен быть исчерпывающим — нужно учесть каждый возможный вариант, иначе код не скомпилируется.

Rust
enum Command {
    Quit,
    Move { x: i32, y: i32 },
    Say(String),
}

fn run(cmd: &Command) -> String {
    match cmd {
        Command::Quit => String::from("bye"),
        Command::Move { x, y } => format!("move to ({x}, {y})"),
        Command::Say(text) => format!("say: {text}"),
    }
}

fn main() {
    let commands = [
        Command::Say(String::from("hello")),
        Command::Move { x: 3, y: -1 },
        Command::Quit,
    ];
    for cmd in &commands {
        println!("{}", run(cmd));
    }
}
Ожидаемый результат
say: hello
move to (3, -1)
bye
format! работает как println!, но не печатает текст, а возвращает его как String. В ветвях match имена x, y и text связываются с данными внутри варианта.
Ошибка E0004
fn day_type(day: u8) -> char {
    match day {
        1..=5 => 'W',
        6 | 7 => 'H',
    }
}
Исчерпывающий с `_`
fn day_type(day: u8) -> char {
    match day {
        1..=5 => 'W',
        6 | 7 => 'H',
        _ => '?',
    }
}
u8 может принимать значения от 0 до 255, поэтому компилятор пишет: non-exhaustive patterns: 0_u8 and 8_u8..=u8::MAX not covered. _ означает «всё остальное». В образцах можно использовать диапазоны (1..=5) и «или» (6 | 7).

Option вместо null

В Rust нет null. Если значение может быть, а может и не быть, используют перечисление Option<T> из стандартной библиотеки: Some(значение) или None. Значение внутри нельзя использовать напрямую — компилятор заставляет учесть и случай None. Если интересен только один вариант, вместо полного match пиши короткий if let.

Rust
fn find_index(names: &[&str], target: &str) -> Option<usize> {
    for (i, name) in names.iter().enumerate() {
        if *name == target {
            return Some(i);
        }
    }
    None
}

fn main() {
    let names = ["Elvin", "Leyla", "Murad"];
    match find_index(&names, "Leyla") {
        Some(i) => println!("Leyla is at {i}"),
        None => println!("Leyla not found"),
    }
    if let Some(i) = find_index(&names, "Nigar") {
        println!("Nigar is at {i}");
    } else {
        println!("Nigar not found");
    }
    let murad = find_index(&names, "Murad");
    println!("{:?} {}", murad, murad.unwrap_or(0));
}
Ожидаемый результат
Leyla is at 1
Nigar not found
Some(2) 2
unwrap_or(0) возвращает значение из Some, а при None — указанное запасное значение. В стандартной библиотеке есть и готовый метод, делающий то же самое: names.iter().position(|n| *n == target).

Главное

  • struct объединяет именованные поля в один тип; #[derive(Debug)] позволяет печатать его через {:?}.
  • Методы пишут в блоке impl: &self читает, &mut self изменяет; функцию без self вызывают как Тип::имя().
  • Варианты enum могут нести данные; match должен охватывать все варианты, а _ ловит остальные.
  • В Rust нет null: возможно отсутствующее значение — это Option<T>, то есть Some(x) или None.
  • unwrap() паникует при None; match, if let и unwrap_or безопаснее.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Какая сигнатура метода позволяет изменять поля структуры?