1.5 Взаймы
В мастерской принято передавать работу. Подмастерье полирует, другой мастер осматривает - каждый делает своё, и никто не объясняет правило, потому что оно само собой разумеется: кто получил изделие, тот им и распоряжается. Задача несёт имя, форму и состояние. Пришло время передавать её дальше - и выяснять, чем передача отличается от одолжения. Само собой разумеющееся редко объясняется заранее.
Попытка
До этого момента вся логика была описана в main: создавала задачу через поля и напрямую меняла task.status.
Это тяжело читать - нужны явные комментарии - и ещё сложнее поддерживать: если задач будет много,
нужно будет пройтись по всем местам и править логику. Решение простое - вынести это в отдельные функции:
одну для создания задачи, другую - для её завершения.
Первая - create_task. Принимает id и заголовок, возвращает готовую задачу:
fn create_task(id: u64, title: &str) -> Task {
Task {
id,
title: String::from(title),
status: Status::Todo,
}
}
title: &str - строка, переданная по ссылке: функция читает её, но не забирает.
String::from(title) создаёт владеющую копию - как делал main раньше. Это компилируется.
В большинстве языков функция возвращает значение через return. В Rust return тоже существует,
но обычно не нужен: последнее выражение в теле функции без ; и есть возвращаемое значение.
create_task работает именно так - конструктор Task { ... } стоит последним и без точки с запятой,
поэтому функция возвращает его вызывающему.
Вторая - task_complete. Как известно из прошлой главы, для изменения значения нужен mut -
это относится и к аргументам функции:
fn task_complete(mut task: Task) {
task.status = Status::Done;
}
Параметр mut task: Task - задача принята по значению, mut позволяет изменить её внутри.
Та же логика, что у create_task, - только зеркально: последняя строка заканчивается ;,
значит функция ничего не возвращает. create_task оставила Task { ... } без ; - и вернула задачу;
task_complete ставит ; - и не возвращает ничего. Компилятор принимает. Пробуем использовать обе:
fn main() {
let mut task = create_task(1, "Buy coffee");
println!("{:#?}", task);
task_complete(task);
println!("{:#?}", task);
}
main стал проще и читается гораздо приятнее.
Однако у компилятора есть вопрос:
error[E0382]: borrow of moved value: `task`
--> src/main.rs:XX:XX
|
XX | let mut task = create_task(1, "Buy coffee");
| -------- move occurs because `task` has type `Task`, which does not implement the `Copy` trait
XX | println!("{:#?}", task);
XX | task_complete(task);
| ---- value moved here
XX | println!("{:#?}", task);
| ^^^^ value borrowed here after move
Что знает компилятор
В Rust у каждого значения один хозяин. Передать значение в функцию - значит передать полные права на объект (владение): функция становится хозяином, прежний владелец теряет доступ. Когда функция заканчивается, все её внутренние переменные уничтожаются вместе с ней.
Конечно, кроме тех, что она возвращает обратно, - именно поэтому create_task создаёт задачу и отдаёт её наружу -
владение переходит к вызывающему. Задача живёт там, где её получили.
А вот task_complete в нынешнем виде забирает задачу насовсем. Функция заканчивается - задача
уничтожается. Второй println! обращается к тому, чего уже нет.
Изменить изделие, не забирая его, - другая история.
Решение
Вместо того чтобы передавать задачу целиком, дадим функции временный доступ на запись - в Rust это называется заимствованием.
Вместо Task - &mut Task:
fn task_complete(task: &mut Task) {
task.status = Status::Done;
}
Функция получает ссылку, а не значение. Она может изменить задачу через эту ссылку, но не
владеет ею. Когда функция заканчивается, исчезает ссылка; задача в main продолжает жить.
Вызов теперь требует явного &mut:
fn main() {
let mut task = create_task(1, "Buy coffee");
println!("Task #{}: {}", task.id, task.title);
println!("{:#?}", task);
task_complete(&mut task);
println!("{:#?}", task);
}
&mut task - изменяемая ссылка на задачу. let mut task в main по-прежнему необходим:
без него изменяемую ссылку создать нельзя.
Task #1: Buy coffee
Task {
id: 1,
title: "Buy coffee",
status: Todo,
}
Task {
id: 1,
title: "Buy coffee",
status: Done,
}
make ci проходит. Задача создаётся через функцию, завершается через функцию - и остаётся
доступной вызывающему.
Полный код
tqдля этой главы -1-a-task/05-lending-the-work/.
Lore: три способа передать значение
Функция принимает значение одним из трёх способов - и каждый имеет последствия:
Task- перемещение. Функция становится владельцем. После вызова значение недоступно вызывающему.&Task- заимствование на чтение. Функция может читать значение; вызывающий сохраняет владение.&mut Task- заимствование на запись. Функция может изменить значение; вызывающий сохраняет владение, но обязан объявить переменную черезlet mut.
create_task возвращает Task - она не принимает хозяйство, а создаёт новое и отдаёт
вызывающему. Это четвёртый исход: функция как источник, а не получатель.
Вместо Task здесь может стоять любой тип - правило одно для всех.
Lore: mut у привязки и mut в типе
В главе встретились два разных mut, и на первый взгляд они выглядят непоследовательно:
fn task_complete(mut task: Task) // mut перед именем
fn task_complete(task: &mut Task) // mut внутри типа
В первом случае mut - модификатор привязки. Он говорит: “локальная переменная task
изменяема”. Без него строка task.status = Status::Done; не скомпилировалась бы - компилятор
замечает попытку изменить неизменяемое.
Во втором случае mut - часть типа. &mut Task - отдельный тип: изменяемая ссылка на
задачу. Переменная task при этом сама по себе не изменяема - её не нужно переназначать,
достаточно читать и писать через неё. Мутабельность здесь описывает не переменную, а то,
на что она указывает.
Правило простое: mut перед именем - про переменную; mut внутри типа после & - про
значение по ту сторону ссылки.