← Оглавление

1.4 Состояния

Опытный мастер никогда не подписывает инструменты “первый” и “второй”. Начинающий подписывает - и ему это кажется достаточным. Ему кажется достаточным ровно до того момента, когда инструментов становится три, или когда он возвращается спустя месяц, или когда приходит кто-то ещё. Одно из этих случается всегда. Статус задачи сейчас именно такой.

Попытка

После прошлой главы main.rs печатает статус так:

println!("Done: {}", task.status);

Это выведет на печать true или false. Неплохо, но хочется большего: задача должна уметь менять состояние. Добавим строку после инициализации:

task.status = true;

Компилятор подсказывает:

error[E0594]: cannot assign to `task.status`, as `task` is not declared as mutable
  --> src/main.rs:XX:XX
   |
XX |     task.status = true;
   |     ^^^^^^^^^^^^^^^^^^ cannot assign
   |
help: consider changing this to be mutable
   |
XX |     let mut task = Task {
   |         +++

В Rust, по умолчанию, значения нельзя изменять, получить такое право можно лишь явно - let mut:

let mut task = Task {
    id: 1,
    title: String::from("Buy coffee"),
    status: false,
};

Теперь компилятор примет строку task.status = true. Но true - имя значения булева типа, а не имя состояния задачи. С первого взгляда разница невелика - но принципиальна на практике.

Имена

Для именованных состояний в Rust есть тип enum, где можно определить строгий набор вариантов. Добавим его в начало файла, до struct Task. #[derive(Debug)] нужен сразу: компилятор не может сгенерировать Debug для структуры, если хотя бы одно её поле Debug не реализует, - а Task скоро получит поле типа Status:

#[derive(Debug)]
enum Status {
    Todo,
    Done,
}

Это не два значения одного типа - это два отдельных варианта с собственными именами. Заменим bool в структуре и скорректируем main:

struct Task {
    id: u64,
    title: String,
    status: Status,   // было: bool
}

// в main():
let mut task = Task {
    id: 1,
    title: String::from("Buy coffee"),
    status: Status::Todo,  // было: false
};

// ...

task.status = Status::Done;  // было: true

Уберём println!("Done: {}", task.status) - Status не реализует Display, эта строка больше не скомпилируется. Вместо неё поставим match. Попробуем с одним вариантом:

match task.status {
    Status::Todo => println!("todo"),
}

Заклинание

Компилятор подсказывает недочёт:

error[E0004]: non-exhaustive patterns: `Done` not covered
  --> src/main.rs:XX:XX
   |
XX |     match task.status {
   |           ^^^^^^^^^^^ pattern `Done` not covered
   |
note: `Status` defined here
  --> src/main.rs:XX:XX
   |
XX | enum Status {
   |      ------
XX |     Todo,
XX |     Done,
   |     ^^^^ not covered
   = note: the matched value is of type `Status`

match в Rust требует охватить все возможные варианты. Это гарантия: если Status когда-нибудь получит новый вариант, компилятор найдёт каждый match, который про него не знает, и откажется компилировать. Забытый вариант не проскользнёт молча.

Добавляем недостающий вариант и смотрим на переход:

#[derive(Debug)]
enum Status {
    Todo,
    Done,
}

#[derive(Debug)]
struct Task {
    id: u64,
    title: String,
    status: Status,
}

fn main() {
    let mut task = Task {
        id: 1,
        title: String::from("Buy coffee"),
        status: Status::Todo,
    };

    println!("Task #{}: {}", task.id, task.title);
    println!("{:#?}", task);

    task.status = Status::Done;

    println!("{:#?}", task);
    match task.status {
        Status::Todo => println!("todo"),
        Status::Done => println!("done"),
    }
}
Task #1: Buy coffee
Task {
    id: 1,
    title: "Buy coffee",
    status: Todo,
}
Task {
    id: 1,
    title: "Buy coffee",
    status: Done,
}
done

Первый {:#?} - задача до изменения. Второй - после. match подтверждает новое состояние. make ci проходит.

Полный код tq для этой главы - 1-a-task/04-states-of-being/.


Lore: _ в match

Иногда хочется обработать один вариант, а остальные проигнорировать. Для этого есть _ - паттерн, совпадающий с чем угодно:

match task.status {
    Status::Done => println!("done"),
    _ => {}
}

Это работает - но для Status это плохая идея. Смысл исчерпывающего match именно в том, чтобы компилятор предупреждал при добавлении нового варианта. _ эту защиту убирает: появится InProgress - и он молча попадёт под _ без единого предупреждения.

_ уместен, когда вариантов действительно много и большинство обрабатываются одинаково, или когда enum чужой и его нельзя контролировать. Для собственного Status лучше всегда писать все варианты явно.