Из прошлого урока вы знаете: у каждого значения один владелец, а когда мы передаём String в функцию, она уходит туда насовсем, и пользоваться ею дальше нельзя. Копировать каждый раз через clone неудобно и дорого. Нужен способ дать функции значение на время, не отдавая его — ровно это делают ссылки.
Ссылка: посмотреть, не забирая
Представьте, что вы одолжили приятелю книгу почитать. Книга по-прежнему ваша: он не может ни продать её, ни выбросить, он лишь держит в руках и читает. Через час вы получите её обратно, и она всё ещё лежит у вас на полке в целости.
В Rust такой «одолженный на время доступ» называется ссылкой (reference), а сам приём — заимствованием (borrowing). Ссылку создаёт знак & перед именем: ¬e читается как «ссылка на note». Тип ссылки пишется так же: &String — «ссылка на строку».
fn len_of(s: &String) -> usize {
s.len()
}
fn main() {
let note = String::from("обед");
let n = len_of(¬e);
println!("{note}: {n}");
}
обед: 8
Разбор по строкам
fn len_of(s: &String) -> usize— параметрsимеет тип&String: функция получает не саму строку, а ссылку на неё. Результат —usize, целое без знака, в которое Rust складывает размеры и длины.s.len()— длина строки. Вызывать метод через ссылку можно так же, как у самой строки, дополнительных действий не нужно. Длина считается в байтах, поэтому у четырёх русских букв получится 8; почему так, мы разберём в одном из следующих уроков.let note = String::from("обед");— владелец строки — переменнаяnote, и так будет до конца.len_of(¬e)— даём функции ссылку наnote. Владение не меняется: функция только заглянула в строку.println!("{note}: {n}");— после вызоваnoteпо-прежнему наша — в отличие от прошлого урока, где строка после передачи в функцию пропадала.
Ссылка не владеет
Когда функция с параметром &String заканчивается, строка не удаляется: удалять должен владелец, а ссылка им не является. Поэтому сколько раз ни вызывайте len_of(¬e), строка остаётся на месте.
Изменяемая ссылка: дать возможность писать
Бывает, что функции нужно не только прочитать, но и дописать. Это уже не «почитать книгу», а «дать тетрадь, в которой можно писать». Для этого есть изменяемая ссылка (mutable reference): пишется &mut, и в типе (&mut String), и при вызове (&mut note).
Вы уже видели её в программе с вводом: read_line(&mut line). Теперь понятно, что это значит: мы даём функции право записать прочитанный текст в нашу строку line, но строка остаётся нашей.
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, а вы передали ¬e, будет error[E0308]: mismatched types («types differ in mutability»). Обе ошибки разобраны ниже, в разделе про частые ошибки.
Правила заимствования
Если бы любой мог в любой момент менять значение, пока другие его читают, получился бы хаос: вы читаете абзац, а его в этот момент переписывают. Поэтому у заимствования есть строгое правило, как в читальном зале: либо много читателей одновременно, либо один редактор, но не оба сразу.
Официально это звучит так: в любой момент у значения может быть сколько угодно обычных ссылок & ИЛИ ровно одна изменяемая &mut. Проверяет это компилятор, ещё до запуска программы.
| Что есть у значения | Можно ли добавить ещё |
|---|---|
несколько & | да, ещё &; изменяемую — нет |
одна &mut | нет ни &, ни второй &mut |
Много читателей
fn main() {
let note = String::from("обед");
let a = ¬e;
let b = ¬e;
println!("{a} {b} {note}");
}
обед обед обед
Две ссылки a и b и сам владелец читают строку одновременно, и это безопасно, ведь никто её не меняет.
Читатель и редактор вместе: E0502
Теперь попробуем изменить строку, пока ссылка-читатель ещё понадобится дальше:
fn main() {
let mut note = String::from("обед");
let reader = ¬e;
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 = ¬e;
| ----- 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 после изменения уже не использовался, ошибки не было бы: ссылка «живёт» только до последнего своего использования. Поэтому исправление простое — сначала дочитать, потом менять:
fn main() {
let mut note = String::from("обед");
let reader = ¬e;
println!("{reader}");
note.push_str(" в столовой");
println!("{note}");
}
обед
обед в столовой
Два редактора: E0499
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, а мы уже берём вторую. Исправление то же: закончить работу с первой, потом брать вторую.
fn main() {
let mut note = String::from("обед");
let first = &mut note;
first.push_str(" в столовой");
let second = &mut note;
second.push_str("!");
println!("{note}");
}
обед в столовой!
Ссылка не может пережить владельца: E0106
Есть и второе правило: ссылка не должна жить дольше значения, на которое указывает. Вернуть ссылку на то, что создано внутри функции, нельзя: при выходе из функции значение удаляется, а ссылка осталась бы «висеть» в пустоте (так и называется — висячая ссылка).
fn make_note() -> &String {
let note = String::from("обед");
¬e
}
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, а не ссылку на неё:
fn make_note() -> String {
String::from("обед")
}
fn main() {
let note = make_note();
println!("{note}");
}
обед
Как жить с этими правилами
Если компилятор ругается на заимствование, спросите себя: кто сейчас владелец, у кого есть доступ и до какой строки он ещё нужен? Чаще всего достаточно переставить строки местами или закончить работу с одной ссылкой до начала другой.
Разыменование: звёздочка
Ссылка — это «адрес», стрелка, которая указывает на значение. Чтобы поработать не со стрелкой, а с тем, куда она указывает, нужно пройти по ней. Эта операция называется разыменованием (dereference) и записывается звёздочкой * перед ссылкой.
Для строк мы звёздочку не писали: методы вроде push_str сами проходят по ссылке. А вот с числами и операторами (+=, -=) звёздочка нужна. Сделаем шаг проекта — функцию, которая прибавляет трату к накопленной сумме:
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.
Если забыть звёздочку, компилятор подскажет:
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 в цикле.
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(¬e);
print_note(¬e);
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(¬e);
}
Запустите cargo run и введите по очереди 350, 1200, абв, 23000, стоп (каждое — в новой строке). Должно получиться так:
Копилка 0.2
Вводите траты по одной, для выхода напишите стоп.
Заметка: обед
Заметка: обед
Введите сумму (или стоп):
Записано: 350 руб. Всего: 350 руб.
Введите сумму (или стоп):
Записано: 1200 руб. Всего: 1550 руб.
Введите сумму (или стоп):
Это не число, попробуйте ещё раз
Введите сумму (или стоп):
Записано: 23000 руб. Всего: 24550 руб.
Введите сумму (или стоп):
Итого потрачено: 24550 руб. из 30000 руб.
Осталось: 5450 руб.
Использовано лимита: 81%
Статус: почти лимит
Заметка: обед
Разбор по строкам
print_note(¬e);— вызываем трижды: заметка не пропадает, потому что мы только одалживаем её функции.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(¬e)в самый конец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.