$ sudo teach IT
Модуль 3 · Владение · Урок 3.2

Ссылки и заимствование

Вы научитесь давать функции значение «на время» — только посмотреть или с правом изменить, — и поймёте, что именно проверяет компилятор.

Теория + практика~30 минутНовичок

Из прошлого урока вы знаете: у каждого значения один владелец, а когда мы передаём String в функцию, она уходит туда насовсем, и пользоваться ею дальше нельзя. Копировать каждый раз через clone неудобно и дорого. Нужен способ дать функции значение на время, не отдавая его — ровно это делают ссылки.

Ссылка: посмотреть, не забирая

Представьте, что вы одолжили приятелю книгу почитать. Книга по-прежнему ваша: он не может ни продать её, ни выбросить, он лишь держит в руках и читает. Через час вы получите её обратно, и она всё ещё лежит у вас на полке в целости.

В Rust такой «одолженный на время доступ» называется ссылкой (reference), а сам приём — заимствованием (borrowing). Ссылку создаёт знак & перед именем: &note читается как «ссылка на note». Тип ссылки пишется так же: &String — «ссылка на строку».

Rust · main.rs
fn len_of(s: &String) -> usize {
    s.len()
}

fn main() {
    let note = String::from("обед");
    let n = len_of(&note);
    println!("{note}: {n}");
}
Вывод
обед: 8

Разбор по строкам

  • fn len_of(s: &String) -> usize — параметр s имеет тип &String: функция получает не саму строку, а ссылку на неё. Результат — usize, целое без знака, в которое Rust складывает размеры и длины.
  • s.len() — длина строки. Вызывать метод через ссылку можно так же, как у самой строки, дополнительных действий не нужно. Длина считается в байтах, поэтому у четырёх русских букв получится 8; почему так, мы разберём в одном из следующих уроков.
  • let note = String::from("обед"); — владелец строки — переменная note, и так будет до конца.
  • len_of(&note) — даём функции ссылку на note. Владение не меняется: функция только заглянула в строку.
  • println!("{note}: {n}"); — после вызова note по-прежнему наша — в отличие от прошлого урока, где строка после передачи в функцию пропадала.

Ссылка не владеет

Когда функция с параметром &String заканчивается, строка не удаляется: удалять должен владелец, а ссылка им не является. Поэтому сколько раз ни вызывайте len_of(&note), строка остаётся на месте.

Изменяемая ссылка: дать возможность писать

Бывает, что функции нужно не только прочитать, но и дописать. Это уже не «почитать книгу», а «дать тетрадь, в которой можно писать». Для этого есть изменяемая ссылка (mutable reference): пишется &mut, и в типе (&mut String), и при вызове (&mut note).

Вы уже видели её в программе с вводом: read_line(&mut line). Теперь понятно, что это значит: мы даём функции право записать прочитанный текст в нашу строку line, но строка остаётся нашей.

Rust · main.rs
fn add_tag(note: &mut String) {
    note.push_str(" #еда");
}

fn main() {
    let mut note = String::from("обед");
    add_tag(&mut note);
    println!("{note}");
}
Вывод
обед #еда

Разбор по строкам

  • fn add_tag(note: &mut String) — функция берёт изменяемую ссылку на строку: ей разрешено менять чужую строку.
  • note.push_str(" #еда"); — push_str дописывает текст в конец строки-владельца. Через &mut это работает как с самой строкой.
  • let mut note — слово mut здесь обязательно: нельзя дать право менять то, что менять нельзя.
  • add_tag(&mut note); — &mut нужно и при вызове — так в коде сразу видно, что функция может изменить значение.
  • println!("{note}"); — после вызова строка уже изменена: обед #еда.

Три знака, которые легко перепутать

Если у переменной нет mut, а вы пишете &mut, получите error[E0596]. Если в функции &mut String, а вы передали &note, будет error[E0308]: mismatched types («types differ in mutability»). Обе ошибки разобраны ниже, в разделе про частые ошибки.

Правила заимствования

Если бы любой мог в любой момент менять значение, пока другие его читают, получился бы хаос: вы читаете абзац, а его в этот момент переписывают. Поэтому у заимствования есть строгое правило, как в читальном зале: либо много читателей одновременно, либо один редактор, но не оба сразу.

Официально это звучит так: в любой момент у значения может быть сколько угодно обычных ссылок & ИЛИ ровно одна изменяемая &mut. Проверяет это компилятор, ещё до запуска программы.

Что есть у значенияМожно ли добавить ещё
несколько &да, ещё &; изменяемую — нет
одна &mutнет ни &, ни второй &mut

Много читателей

Rust · main.rs
fn main() {
    let note = String::from("обед");
    let a = &note;
    let b = &note;
    println!("{a} {b} {note}");
}
Вывод
обед обед обед

Две ссылки a и b и сам владелец читают строку одновременно, и это безопасно, ведь никто её не меняет.

Читатель и редактор вместе: E0502

Теперь попробуем изменить строку, пока ссылка-читатель ещё понадобится дальше:

Rust · main.rs
fn main() {
    let mut note = String::from("обед");
    let reader = &note;
    note.push_str(" в столовой");
    println!("{reader}");
}
Ошибка компилятора
error[E0502]: cannot borrow `note` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:5
  |
3 |     let reader = &note;
  |                  ----- immutable borrow occurs here
4 |     note.push_str(" в столовой");
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
5 |     println!("{reader}");
  |                ------ immutable borrow later used here

Компилятор показывает три места: где взята обычная ссылка (immutable borrow occurs here), где мы пытаемся менять (mutable borrow occurs here) и где обычная ссылка снова нужна (immutable borrow later used here). Если бы reader после изменения уже не использовался, ошибки не было бы: ссылка «живёт» только до последнего своего использования. Поэтому исправление простое — сначала дочитать, потом менять:

Rust · main.rs
fn main() {
    let mut note = String::from("обед");
    let reader = &note;
    println!("{reader}");
    note.push_str(" в столовой");
    println!("{note}");
}
Вывод
обед
обед в столовой

Два редактора: E0499

Rust · main.rs
fn main() {
    let mut note = String::from("обед");
    let first = &mut note;
    let second = &mut note;
    first.push_str(" в столовой");
    second.push_str("!");
}
Ошибка компилятора
error[E0499]: cannot borrow `note` as mutable more than once at a time
 --> src/main.rs:4:18
  |
3 |     let first = &mut note;
  |                 --------- first mutable borrow occurs here
4 |     let second = &mut note;
  |                  ^^^^^^^^^ second mutable borrow occurs here
5 |     first.push_str(" в столовой");
  |     ----- first borrow later used here

error[E0499]: cannot borrow note as mutable more than once at a time — «нельзя брать изменяемую ссылку больше одного раза одновременно». Первая ссылка first ещё нужна в строке 5, а мы уже берём вторую. Исправление то же: закончить работу с первой, потом брать вторую.

Rust · main.rs
fn main() {
    let mut note = String::from("обед");
    let first = &mut note;
    first.push_str(" в столовой");
    let second = &mut note;
    second.push_str("!");
    println!("{note}");
}
Вывод
обед в столовой!

Ссылка не может пережить владельца: E0106

Есть и второе правило: ссылка не должна жить дольше значения, на которое указывает. Вернуть ссылку на то, что создано внутри функции, нельзя: при выходе из функции значение удаляется, а ссылка осталась бы «висеть» в пустоте (так и называется — висячая ссылка).

Rust · main.rs
fn make_note() -> &String {
    let note = String::from("обед");
    &note
}

fn main() {
    let note = make_note();
    println!("{note}");
}
Ошибка компилятора
error[E0106]: missing lifetime specifier
 --> src/main.rs:1:19
  |
1 | fn make_note() -> &String {
  |                   ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
  |
1 | fn make_note() -> &'static String {
  |                    +++++++
help: instead, you are more likely to want to return an owned value
  |
1 - fn make_note() -> &String {
1 + fn make_note() -> String {
  |

Сообщение говорит о «lifetime» — времени жизни ссылки. Эту тему мы здесь не разбираем, она относится к более продвинутому курсу. Нам важен совет в конце: вы, скорее всего, хотите вернуть владеемое значение. Так и делаем — возвращаем саму String, а не ссылку на неё:

Rust · main.rs
fn make_note() -> String {
    String::from("обед")
}

fn main() {
    let note = make_note();
    println!("{note}");
}
Вывод
обед

Как жить с этими правилами

Если компилятор ругается на заимствование, спросите себя: кто сейчас владелец, у кого есть доступ и до какой строки он ещё нужен? Чаще всего достаточно переставить строки местами или закончить работу с одной ссылкой до начала другой.

Разыменование: звёздочка

Ссылка — это «адрес», стрелка, которая указывает на значение. Чтобы поработать не со стрелкой, а с тем, куда она указывает, нужно пройти по ней. Эта операция называется разыменованием (dereference) и записывается звёздочкой * перед ссылкой.

Для строк мы звёздочку не писали: методы вроде push_str сами проходят по ссылке. А вот с числами и операторами (+=, -=) звёздочка нужна. Сделаем шаг проекта — функцию, которая прибавляет трату к накопленной сумме:

Rust · main.rs
fn add_expense(spent: &mut u32, amount: u32) {
    *spent += amount;
}

fn main() {
    let mut spent: u32 = 0;
    add_expense(&mut spent, 350);
    add_expense(&mut spent, 1200);
    println!("Потрачено: {spent}");
}
Вывод
Потрачено: 1550

Разбор по строкам

  • fn add_expense(spent: &mut u32, amount: u32) — первый параметр — изменяемая ссылка на число, второй — обычное число. Число копируется само, поэтому ссылка на amount не нужна.
  • *spent += amount; — *spent — «то, на что указывает ссылка», то есть само число в переменной spent из main. К нему и прибавляем.
  • add_expense(&mut spent, 350); — даём право менять spent. Переменная объявлена как let mut.
  • println!("Потрачено: {spent}"); — обе траты накопились: 350 + 1200 = 1550.

Если забыть звёздочку, компилятор подскажет:

Rust · main.rs
fn add_expense(spent: &mut u32, amount: u32) {
    spent += amount;
}

fn main() {
    let mut spent: u32 = 0;
    add_expense(&mut spent, 350);
    println!("Потрачено: {spent}");
}
Ошибка компилятора
error[E0368]: binary assignment operation `+=` cannot be applied to type `&mut u32`
 --> src/main.rs:2:5
  |
2 |     spent += amount;
  |     -----^^^^^^^^^^
  |     |
  |     cannot use `+=` on type `&mut u32`
  |
help: `+=` can be used on `u32` if you dereference the left-hand side
  |
2 |     *spent += amount;
  |     +

error[E0368]: нельзя применить += к типу &mut u32, то есть к самой ссылке. Компилятор сам предлагает исправление: поставить * слева.

Практика: add_expense и print_note в программе «Копилка»

Возьмём программу с вводом сумм из конца прошлого модуля и подключим к ней ссылки. Откройте проект kopilka и файл src/main.rs. Мы добавим две функции — print_note(note: &String) и add_expense(spent: &mut u32, amount: u32) — и заменим в цикле строку spent += amount; вызовом add_expense(&mut spent, amount);. Строку note из прошлого урока теперь можно передавать без clone: достаточно ссылки.

Ниже весь файл целиком: функции print_header, percent, left, print_status и цикл ввода остались такими же, как в конце прошлого модуля. Новые части: две функции, три вызова print_note и вызов add_expense в цикле.

Rust · main.rs
use std::io;

const MONTH_LIMIT: u32 = 30_000;

fn print_header() {
    println!("Копилка 0.2");
    println!("Вводите траты по одной, для выхода напишите стоп.");
}

fn percent(spent: u32, limit: u32) -> u32 {
    spent * 100 / limit
}

fn left(limit: u32, spent: u32) -> u32 {
    if spent > limit {
        0
    } else {
        limit - spent
    }
}

fn print_status(spent: u32, limit: u32) {
    if spent > limit {
        println!("Статус: лимит превышен");
    } else if spent * 100 >= limit * 80 {
        println!("Статус: почти лимит");
    } else {
        println!("Статус: в норме");
    }
}

fn print_note(note: &String) {
    println!("Заметка: {note}");
}

fn add_expense(spent: &mut u32, amount: u32) {
    *spent += amount;
}

fn main() {
    print_header();

    let note = String::from("обед");
    print_note(&note);
    print_note(&note);

    let mut spent: u32 = 0;
    let mut line = String::new();

    loop {
        println!("Введите сумму (или стоп):");
        line.clear();

        let bytes = io::stdin()
            .read_line(&mut line)
            .expect("не удалось прочитать строку");
        if bytes == 0 {
            break;
        }
        if line.trim() == "стоп" {
            break;
        }

        let amount = match line.trim().parse::<u32>() {
            Ok(n) => n,
            Err(_) => {
                println!("Это не число, попробуйте ещё раз");
                continue;
            }
        };

        add_expense(&mut spent, amount);
        println!("Записано: {amount} руб. Всего: {spent} руб.");
    }

    println!("Итого потрачено: {spent} руб. из {MONTH_LIMIT} руб.");
    println!("Осталось: {} руб.", left(MONTH_LIMIT, spent));
    println!("Использовано лимита: {}%", percent(spent, MONTH_LIMIT));
    print_status(spent, MONTH_LIMIT);
    print_note(&note);
}

Запустите cargo run и введите по очереди 350, 1200, абв, 23000, стоп (каждое — в новой строке). Должно получиться так:

Вывод
Копилка 0.2
Вводите траты по одной, для выхода напишите стоп.
Заметка: обед
Заметка: обед
Введите сумму (или стоп):
Записано: 350 руб. Всего: 350 руб.
Введите сумму (или стоп):
Записано: 1200 руб. Всего: 1550 руб.
Введите сумму (или стоп):
Это не число, попробуйте ещё раз
Введите сумму (или стоп):
Записано: 23000 руб. Всего: 24550 руб.
Введите сумму (или стоп):
Итого потрачено: 24550 руб. из 30000 руб.
Осталось: 5450 руб.
Использовано лимита: 81%
Статус: почти лимит
Заметка: обед

Разбор по строкам

  • print_note(&note); — вызываем трижды: заметка не пропадает, потому что мы только одалживаем её функции.
  • add_expense(&mut spent, amount); — функция сама прибавляет сумму к spent, а в цикле остаётся одна понятная строка.
  • read_line(&mut line) — то же заимствование, что и в add_expense: функция чтения получает право записать текст в line.

Проверьте себя

  • Уберите &mut из вызова add_expense(&mut spent, amount) и запустите cargo check. Какую ошибку и с каким номером вы увидели?
  • Замените в add_expense строку *spent += amount; на spent += amount;. Что говорит компилятор?
  • Сделайте let spent: u32 = 0; без mut. На какую строку теперь ругается компилятор?
  • Добавьте ещё один вызов print_note(&note) в самый конец main и убедитесь, что note всё ещё доступна.

Что дальше в проекте

Позже, когда мы научимся хранить много трат сразу, этот же приём понадобится постоянно: функции будут брать список трат по ссылке, не забирая и не копируя его.

Частые ошибки

1. Забыли &mut при вызове

Что видно. Вызов add_expense(spent, 350) даёт:

Ошибка компилятора
error[E0308]: mismatched types
 --> src/main.rs:7:17
  |
7 |     add_expense(spent, 350);
  |     ----------- ^^^^^ expected `&mut u32`, found `u32`
  |     |
  |     arguments to this function are incorrect
  |
note: function defined here
 --> src/main.rs:1:4
  |
1 | fn add_expense(spent: &mut u32, amount: u32) {
  |    ^^^^^^^^^^^ ---------------
help: consider mutably borrowing here
  |
7 |     add_expense(&mut spent, 350);
  |                 ++++

Почему. Функция ждёт ссылку, а мы передали число. Как исправить. Напишите add_expense(&mut spent, 350) — компилятор сам подсказывает это в строке help.

2. Две изменяемые ссылки сразу

Что видно. error[E0499]: cannot borrow note as mutable more than once at a time. Почему. Редактор может быть только один. Как исправить. Закончите работу с первой ссылкой до того, как возьмёте вторую.

3. Забыли звёздочку

Что видно. error[E0368]: binary assignment operation += cannot be applied to type &mut u32. Почему. Мы пытаемся прибавить число к стрелке, а не к значению. Как исправить. Поставьте *: *spent += amount;.

4. &mut на переменную без mut

Что видно. Вызов add_tag(&mut note) при let note:

Ошибка компилятора
error[E0596]: cannot borrow `note` as mutable, as it is not declared as mutable
 --> src/main.rs:7:13
  |
7 |     add_tag(&mut note);
  |             ^^^^^^^^^ cannot borrow as mutable
  |
help: consider changing this to be mutable
  |
6 |     let mut note = String::from("обед");
  |         +++

Почему. Менять можно только то, что объявлено изменяемым. Как исправить. Допишите mut: let mut note.

5. Возвращаем ссылку на созданное внутри

Что видно. error[E0106]: missing lifetime specifier у функции вида fn make_note() -> &String. Почему. Значение, созданное внутри, удалится при выходе, и ссылке будет не на что указывать. Как исправить. Возвращайте саму String, как в примере выше.

Что важно запомнить

  • Ссылка & даёт посмотреть значение, не забирая его; владелец остаётся прежним.
  • Изменяемая ссылка &mut даёт право менять значение; &mut пишется и в типе параметра, и при вызове, а переменная должна быть объявлена с mut.
  • В любой момент у значения либо сколько угодно &, либо ровно одна &mut. Нарушение: E0502 (читатель и редактор) и E0499 (два редактора).
  • Ссылка живёт до своего последнего использования, а не дольше владельца значения. Вернуть ссылку на локальное значение нельзя (E0106).
  • Звёздочка * разыменовывает ссылку: *spent += amount. Без неё получите E0368.

Дальше посмотрим, почему в параметрах мы чаще пишем &str, а не &String, и познакомимся со срезами: это удобнее и работает с любой строкой.

Подробнее о ссылках — в главе книги References and Borrowing.

На следующий урок