В этом уроке мы соберем учебную модель задачи: Task будет хранить название и статус, Status будет enum с вариантами, а impl добавит методы, которые делают код читаемее
Это важный шаг после коллекций. Vec<String> подходит для простого списка, но реальная задача почти всегда имеет поля, состояние и поведение. Rust дает для этого struct, enum и методы через impl
- Что получится в конце
- struct: связанные данные под одним именем
- enum: состояние без магических строк
- impl: методы рядом с типом
- derive Debug: почему println не печатает struct сам
- Мини-практика: показать статус текстом
- Enum с данными
- Associated function и обычный метод
- Struct update syntax
- Match по ссылке на enum
- Когда enum лучше нескольких bool
- Частые ошибки и порядок проверки
- Мини-практика: список Task в Vec
- Проверка инвариантов через методы
- Что может быть еще интересно по этой теме
- Что почитать дальше по Rust
Что получится в конце
Создайте проект:
cargo new rust_struct_enum
cd rust_struct_enum
Код src/main.rs:
#[derive(Debug)]
enum Status {
Todo,
InProgress,
Done,
}
#[derive(Debug)]
struct Task {
title: String,
status: Status,
}
impl Task {
fn new(title: &str) -> Self {
Self {
title: String::from(title),
status: Status::Todo,
}
}
fn start(&mut self) {
self.status = Status::InProgress;
}
fn finish(&mut self) {
self.status = Status::Done;
}
}
fn main() {
let mut task = Task::new("write Rust lesson");
task.start();
task.finish();
println!("{task:?}");
}
Запуск:
cargo run
struct: связанные данные под одним именем
struct описывает форму данных:
struct Task {
title: String,
status: Status,
}
Теперь задача — не просто строка. У нее есть название и статус. Это делает код ближе к реальной предметной области
Создание структуры можно написать напрямую:
let task = Task {
title: String::from("write lesson"),
status: Status::Todo,
};
Но если создание повторяется, удобнее вынести его в метод new
enum: состояние без магических строк
Статус можно было хранить строкой:
let status = "done";
Но строка допускает опечатки: "Done", "done", "finished", "don". Enum ограничивает варианты:
enum Status {
Todo,
InProgress,
Done,
}
Теперь компилятор знает все допустимые статусы. Если вы используете match, он попросит обработать каждый вариант
fn status_label(status: &Status) -> &'static str {
match status {
Status::Todo => "todo",
Status::InProgress => "in progress",
Status::Done => "done",
}
}
impl: методы рядом с типом
Блок:
impl Task {
fn start(&mut self) {
self.status = Status::InProgress;
}
}
добавляет метод к Task. &mut self означает, что метод меняет объект. Вызов выглядит так:
task.start();
Self внутри impl означает текущий тип, то есть Task. Поэтому fn new(title: &str) -> Self возвращает новый Task
В Rust нет классов в привычном C++/Java-смысле, но есть типы данных и методы. Этого достаточно, чтобы держать поведение рядом с данными
derive Debug: почему println не печатает struct сам
Чтобы напечатать структуру через {:?}, мы добавляем:
#[derive(Debug)]
Rust не печатает любой тип автоматически. Он требует, чтобы тип реализовал поведение форматирования для debug-вывода. derive(Debug) просит компилятор сгенерировать такую реализацию
Для пользовательского вывода есть trait Display, но на первом этапе Debug достаточно для диагностики и учебных примеров
Мини-практика: показать статус текстом
Добавьте метод:
impl Task {
fn label(&self) -> &'static str {
match self.status {
Status::Todo => "todo",
Status::InProgress => "in progress",
Status::Done => "done",
}
}
}
И вызов:
println!("{}: {}", task.title, task.label());
Метод label получает &self, потому что только читает задачу. start и finish получают &mut self, потому что меняют статус. Это хорошая Rust-привычка: явно отделять чтение от изменения
Enum с данными
Enum может хранить данные внутри вариантов:
enum Message {
Text(String),
Retry { count: u32 },
Quit,
}
Тогда match может достать эти данные:
match message {
Message::Text(text) => println!("{text}"),
Message::Retry { count } => println!("Retry: {count}"),
Message::Quit => println!("Quit"),
}
Это делает enum намного сильнее, чем просто набор констант. Option и Result, которые мы уже разбирали, тоже enum-типы
Associated function и обычный метод
Task::new вызывается через тип, а не через объект:
let task = Task::new("write lesson");
Это associated function. У нее нет параметра self, потому что готового объекта еще нет. Она создает новый объект и возвращает Self
Метод вызывается через объект:
task.label();
У метода есть self, &self или &mut self. Это показывает, как метод связан с конкретным значением. Новичку полезно различать эти формы, потому что ошибки Rust часто прямо говорят: метод ожидает &mut self, а у вас нет изменяемого доступа
Struct update syntax
Иногда нужно создать новую структуру на основе старой:
let task = Task::new("write lesson");
let next_task = Task {
title: String::from("publish lesson"),
..task
};
Синтаксис ..task переносит оставшиеся поля из task. Но с ownership нужно быть аккуратным: если поле не реализует Copy, оно может переехать в новую структуру
Для первых уроков не обязательно активно использовать struct update syntax. Но его полезно знать, чтобы не удивляться примерам из документации и чужого кода
Match по ссылке на enum
Если метод только читает статус, можно матчить ссылку:
fn label(&self) -> &'static str {
match &self.status {
Status::Todo => "todo",
Status::InProgress => "in progress",
Status::Done => "done",
}
}
Так мы не забираем владение статусом из структуры. Для enum без данных разница может казаться незаметной, но привычка матчить по ссылке помогает, когда варианты enum начинают хранить String, Vec или другие владеющие значения
Когда enum лучше нескольких bool
Можно было описать задачу так:
struct Task {
title: String,
is_started: bool,
is_done: bool,
}
Но тогда возможны странные состояния: is_done = true, is_started = false. Enum делает состояние одним значением из ограниченного набора. Так меньше шансов получить комбинацию, которую бизнес-логика не понимает
Если у сущности есть взаимоисключающие состояния, enum часто лучше набора флагов
Частые ошибки и порядок проверки
Забыли mut перед task Методы start и finish меняют статус, значит переменная должна быть let mut task
Пытаетесь печатать через {:?} без Debug Добавьте #[derive(Debug)] к структуре и enum, если нужен debug-вывод
Используете строки вместо enum для статуса Строки быстрее написать, но enum лучше защищает от опечаток и неполных состояний
Путаете &self и &mut self Если метод только читает, берите &self. Если меняет поля, нужен &mut self
Мини-практика: список Task в Vec
Теперь соедините структуры с коллекциями:
fn main() {
let mut tasks = vec![
Task::new("write lesson"),
Task::new("check examples"),
Task::new("publish draft"),
];
tasks[0].start();
tasks[0].finish();
for task in &tasks {
println!("{}: {}", task.title, task.label());
}
}
Здесь Vec<Task> хранит уже не строки, а полноценные объекты предметной области. Это важный переход: коллекция отвечает за список, Task отвечает за форму одной задачи, Status отвечает за допустимые состояния
Если программа начинает обрастать условиями вроде if status == "done", вернитесь к enum. Если поля начинают ходить по коду отдельными переменными, вернитесь к struct. Rust хорошо работает, когда смысл данных выражен типами
Проверка инвариантов через методы
Методы помогают не только красиво вызывать код. Они удерживают правила изменения в одном месте. Например, можно запретить завершать задачу, если она еще не начата:
fn finish(&mut self) {
if matches!(self.status, Status::InProgress) {
self.status = Status::Done;
}
}
Теперь внешний код не меняет status напрямую в десяти местах. Он вызывает метод, а метод решает, допустим ли переход. Для маленького урока это кажется строго, но для реального проекта такая дисциплина спасает от случайных состояний
Что может быть еще интересно по этой теме
Struct в Rust похож на struct в C? Идея группировки полей похожа, но Rust добавляет методы через impl, ownership, traits и более строгую типизацию
Enum в Rust похож на enum в C? Только частично. Rust enum может хранить данные в вариантах, поэтому он намного выразительнее
Когда делать метод, а когда функцию? Если операция естественно принадлежит типу, делайте метод в impl. Если функция работает с несколькими независимыми типами, отдельная функция может быть чище
Что дальше после struct и enum? Traits и generics. Они позволяют описывать общее поведение и писать код, который работает с разными типами
Что почитать дальше по Rust
- Vec, String и HashMap в Rust
- Traits и generics в Rust без академического тумана
- CLI на Rust: мини-проект с аргументами командной строки
- Rust и C++: что выбрать для системного кода



