← Оглавление

1.6 Что задача знает о себе

Имя - не умение. Задача носит имя, несёт состояние, умеет описать себя. Но знание о том, как с ней работать, хранится не в ней - а у мастера. Пока изделие одно и мастер не устал, это терпимо. Терпимое имеет привычку становиться невыносимым в самый неудобный момент.

Что нужно

После прошлой главы у задачи есть два помощника: create_task создаёт её, task_complete - завершает. Это шаг вперёд: у операций есть имена, знание о полях не размазано по main.

Но обе функции знают о внутреннем устройстве задачи - о полях, о Status::Todo и Status::Done. Это знание лежит снаружи типа. Пока функций две - терпимо. Каждая новая будет дублировать его самостоятельно.

В Rust поведение типа живёт в блоке impl. Дадим задаче три умения: создавать себя, становиться выполненной и отвечать на вопрос о своём состоянии. create_task станет Task::new, task_complete - методом complete.

Строительство

create_task и task_complete больше не нужны - их логика переедет в impl Task. Добавим блок сразу после struct Task:

impl Task {
    fn new(id: u64, title: &str) -> Task {
        Task {
            id,
            title: String::from(title),
            status: Status::Todo,
        }
    }
}

impl Task открывает блок, где определяется поведение Task. new - функция без параметра self: она не работает с уже существующей задачей, а создаёт новую. Вызывается через ::, а не через точку: Task::new(1, "Buy coffee").

Заменим create_task в main:

let mut task = Task::new(1, "Buy coffee");

let mut - задача изменится следующим шагом; это уже знакомо по прошлой главе. Добавим в тот же impl Task метод complete:

fn complete(&mut self) {
    self.status = Status::Done;
}

complete принимает &mut self - он изменяет задачу, поэтому let mut у вызывающего обязателен. Он уже есть. Вызов: task.complete().

И последний метод туда же - is_done:

fn is_done(&self) -> bool {
    match self.status {
        Status::Done => true,
        Status::Todo => false,
    }
}

is_done принимает &self - только чтение. match здесь знакомый: два варианта, два ответа.

Итог

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

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

impl Task {
    fn new(id: u64, title: &str) -> Task {
        Task {
            id,
            title: String::from(title),
            status: Status::Todo,
        }
    }

    fn complete(&mut self) {
        self.status = Status::Done;
    }

    fn is_done(&self) -> bool {
        match self.status {
            Status::Done => true,
            Status::Todo => false,
        }
    }
}

fn main() {
    let mut task = Task::new(1, "Buy coffee");
    println!("Task #{}: {}", task.id, task.title);
    println!("{:#?}", task);

    task.complete();

    println!("{:#?}", task);
    if task.is_done() {
        println!("done");
    } else {
        println!("todo");
    }
}
Task #1: Buy coffee
Task {
    id: 1,
    title: "Buy coffee",
    status: Todo,
}
Task {
    id: 1,
    title: "Buy coffee",
    status: Done,
}
done

make ci проходит. Три метода готовы - глава 1.11 добавит к ним тесты.

Полный код tq для этой главы - 1-a-task/06-what-a-task-knows-of-itself/.


Lore: &self, &mut self и self

В прошлой главе те же три способа выглядели как Task, &Task, &mut Task - у методов они принимают форму self, &self, &mut self.

У методов в Rust три способа принять себя первым параметром:

new не принимает self вообще: это не метод, а ассоциированная функция. Она принадлежит типу, а не экземпляру - отсюда Task::new, а не task.new.


Lore: макросы - знакомство издалека

В реальном коде is_done нередко пишут иначе:

fn is_done(&self) -> bool {
    matches!(self.status, Status::Done)
}

matches! - это макрос: имя с восклицательным знаком. Макросы в Rust - отдельная большая тема, до неё ещё дойдём. Пока достаточно знать одно: восклицательный знак после имени означает макрос, а не обычную функцию. matches! здесь делает ровно то, что делает наш match - просто короче.