← Оглавление

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/.

Свиток: The simplest way to understand ownership in Rust


Lore: три способа передать значение

Функция принимает значение одним из трёх способов - и каждый имеет последствия:

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 внутри типа после & - про значение по ту сторону ссылки.