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 три способа принять себя первым параметром:
&self- заимствование на чтение. Значение не изменяется, прежний владелец сохраняет его.&mut self- заимствование на запись. Метод может изменить значение; вызывающий должен иметьlet mut.self- передача владения. Метод поглощает значение; после вызова оно недоступно. Редко, но встречается - например, у методов, которые преобразуют тип в другой.
new не принимает self вообще: это не метод, а ассоциированная функция. Она принадлежит
типу, а не экземпляру - отсюда Task::new, а не task.new.
Lore: макросы - знакомство издалека
В реальном коде is_done нередко пишут иначе:
fn is_done(&self) -> bool {
matches!(self.status, Status::Done)
}
matches! - это макрос: имя с восклицательным знаком. Макросы в Rust - отдельная большая
тема, до неё ещё дойдём. Пока достаточно знать одно: восклицательный знак после имени
означает макрос, а не обычную функцию. matches! здесь делает ровно то, что делает наш
match - просто короче.