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 - два разных смысла
fn task_complete(mut task: Task) // как let mut внутри функции
fn task_complete(task: &mut Task) // &mut Task - другой тип, другие правила
В первом случае компилятор делает преобразование:
fn task_complete(mut task: Task) —> fn task_complete(task: Task){let mut task = task}
иными словами - синтаксический сахар для let mut внутри тела функции.
Второй вариант более сложный так как &Task и &mut Task это разные типы:
&Task - разделяемая ссылка, таких может быть несколько одновременно;
&mut Task - исключительная: пока она существует, никакого другого доступа к значению нет.
Именно это и позволяет изменять объект через неё.