Перейти к содержимому

Заимствование в Rust

На странице про владение ты увидел проблему: передача String в функцию её перемещает, и после этого ей нельзя пользоваться. Постоянно перемещать значения туда-сюда (или клонировать их) было бы мучительно. Решение Rust — заимствование (borrowing).

Заимствовать — значит позволить какому-то коду пользоваться значением, не забирая владение. Как дать другу почитать книгу: он прочитает, вернёт, а книга останется твоей.

Ссылка (reference) создаётся, если поставить & перед значением. Ссылка позволяет функции смотреть на данные, не владея ими:

fn print_length(s: &String) { // заимствует s, а не владеет ею
println!("Длина: {}", s.len());
}
fn main() {
let text = String::from("hello");
print_length(&text); // одалживаем text функции
println!("Всё ещё моя: {text}"); // ✅ text здесь по-прежнему действительна
}

Вывод:

Длина: 5
Всё ещё моя: hello
▶ Попробуй сам

&text говорит: «пусть эта функция одолжит text». Функция только заимствовала строку, поэтому владение остаётся у main, и text можно использовать дальше. Сравни со страницей про владение, где тот же код без & вызывал ошибку. (Слово "hello" здесь оставлено на английском: .len() считает байты, а каждая русская буква занимает в UTF-8 два байта, так что у "привет" длина была бы 12.)

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

Заголовок раздела «Изменяемые ссылки: одалживаем, чтобы изменить»

Обычная ссылка & — только для чтения: смотреть можно, трогать нельзя. Если функции нужно изменить одолженное значение, используй изменяемую ссылку &mut:

fn add_excitement(s: &mut String) {
s.push_str("!!!");
}
fn main() {
let mut text = String::from("привет");
add_excitement(&mut text); // одалживаем с правом изменения
println!("{text}"); // привет!!!
}

Вывод:

привет!!!
▶ Попробуй сам

Заметь, что должны совпасть три вещи: text объявлена с mut, функция просит &mut String, а при вызове передаётся &mut text. Чтобы изменить одолженное значение, нужны все три.

Rust следит за двумя правилами для ссылок. Они нужны, чтобы не было гонок данных (data races) — целого класса ошибок, когда два куска кода одновременно трогают одни и те же данные и портят их.

В любой момент у тебя может быть либо:

  • сколько угодно ссылок только для чтения (&), либо
  • ровно одна изменяемая ссылка (&mut),

но не то и другое одновременно.

Проще говоря: много читателей или один писатель, но никогда вместе. Этот код нарушает правило:

fn main() {
let mut text = String::from("hello");
let r1 = &text; // заимствование только для чтения
let r2 = &mut text; // ❌ нельзя заимствовать как изменяемую, пока жива r1
println!("{r1} {r2}");
}

Компилятор тебя остановит:

error[E0502]: cannot borrow `text` as mutable because it is also borrowed as immutable

Сообщение значит: «нельзя заимствовать text как изменяемую, потому что она уже заимствована как неизменяемая».

Можно подумать, что заимствование длится до закрывающей } своей области видимости. Это не так. Заимствование длится только до последнего места, где используется ссылка. После этого значение снова можно заимствовать, даже с правом изменения.

Значит, сломанный пример выше можно починить, просто закончив работу с r1 до создания r2:

fn main() {
let mut text = String::from("привет");
let r1 = &text;
println!("Читаем: {r1}"); // последнее использование r1, заимствование заканчивается здесь
let r2 = &mut text; // ✅ всё в порядке: больше никто не читает
r2.push_str(", мир");
println!("{text}");
}

Вывод:

Читаем: привет
привет, мир
▶ Попробуй сам

Правило по-прежнему «много читателей или один писатель». Просто проверяется оно на том отрезке кода, где каждая ссылка реально используется. Читатель r1 закончил к тому моменту, когда появился писатель r2, а r2 закончил до того, как последний println! снова читает text. Ничего не пересекается, и Rust доволен.

Это самый частый способ исправить ошибку заимствования: поменять порядок. Сделай всё нужное чтение, а потом запись (или наоборот), вместо того чтобы их перемешивать.

Эти правила проверяются целиком при компиляции, поэтому во время работы программы ничего не стоят. Так Rust может обещать «никаких гонок данных» и при этом быть таким же быстрым, как C: безопасность доказана ещё до запуска программы.

Сначала попробуй решить каждую задачу сам — в Rust Playground или в локальном проекте. Если компилятор жалуется, прочитай ошибку: обычно она точно показывает, где пересекаются заимствования.

Напиши функцию total_letters, которая заимствует Vec<String> и возвращает, сколько всего букв во всех словах вместе. После её вызова main должна по-прежнему уметь вывести вектор.

Подсказка: тип параметра — &Vec<String>, а вызывать её нужно с &words. Перебирай одолженный вектор циклом for word in list и складывай word.len().

Показать решение
fn total_letters(list: &Vec<String>) -> usize {
let mut total = 0;
for word in list {
total += word.len();
}
total
}
fn main() {
let words = vec![String::from("borrow"), String::from("check")];
let count = total_letters(&words); // одалживаем вектор
println!("Букв: {count}");
println!("{:?} всё ещё принадлежит main", words);
}

Вывод:

Букв: 11
["borrow", "check"] всё ещё принадлежит main
▶ Попробуй сам

Функции нужно только посмотреть на слова, так что хватает заимствования & только для чтения. Если бы ты написал list: Vec<String>, вектор переместился бы в функцию, и последний println! не скомпилировался бы. Слова здесь английские по той же причине, что и выше: len() считает байты, а не буквы.

Напиши две функции: add_item, которая добавляет товар в список покупок, и print_list, которая выводит все товары. Добавь три товара, а потом выведи список.

Подсказка: одна функция меняет список, а другая только читает. Какой из них нужен &mut Vec<String>, а какой хватит &Vec<String>? Чтобы превратить &str в String, используй String::from(item).

Показать решение
fn add_item(list: &mut Vec<String>, item: &str) {
list.push(String::from(item));
}
fn print_list(list: &Vec<String>) {
for item in list {
println!("- {item}");
}
}
fn main() {
let mut list = Vec::new();
add_item(&mut list, "молоко");
add_item(&mut list, "хлеб");
add_item(&mut list, "яблоки");
print_list(&list);
}

Вывод:

- молоко
- хлеб
- яблоки
▶ Попробуй сам

add_item меняет список, поэтому получает заимствование &mut, а list в main должна быть mut. print_list только читает, так что хватает обычного &. Каждое заимствование &mut list заканчивается, когда возвращается вызов, — поэтому следующий вызов может снова его одолжить.

Эта программа не компилируется. Исправь её так, чтобы она выводила Счёт: 10 очков, сохранив и first, и second.

fn main() {
let mut message = String::from("Счёт:");
let first = &mut message;
let second = &mut message;
first.push_str(" 10");
second.push_str(" очков");
println!("{message}");
}
error[E0499]: cannot borrow `message` as mutable more than once at a time
--> src/main.rs:4:18
|
3 | let first = &mut message;
| ------------ first mutable borrow occurs here
4 | let second = &mut message;
| ^^^^^^^^^^^^ second mutable borrow occurs here
5 | first.push_str(" 10");
| ----- first borrow later used here

Компилятор говорит, что message нельзя заимствовать как изменяемую больше одного раза одновременно, и показывает: первое заимствование в строке 3, второе — в строке 4, а первое всё ещё используется в строке 5.

Подсказка: в каждый момент только один писатель. Посмотри раздел Когда заканчивается заимствование? выше: можешь ли ты закончить с first до того, как создашь second?

Показать решение
fn main() {
let mut message = String::from("Счёт:");
let first = &mut message;
first.push_str(" 10"); // последнее использование first: заимствование заканчивается здесь
let second = &mut message;
second.push_str(" очков");
println!("{message}");
}

Вывод:

Счёт: 10 очков
▶ Попробуй сам

В исходной версии first ещё собирались использовать (в строке 5), когда в строке 4 создавалась second. Это два писателя одновременно. Если перенести first.push_str выше, first закончит работу до появления second, и &mut в каждый момент будет только одна.

  • Заимствование (&) позволяет коду пользоваться значением, не забирая владение, — значение тебе возвращается.
  • Обычная ссылка & — только для чтения; чтобы изменить одолженное значение, используй &mut.
  • Чтобы менять значение через ссылку, mut/&mut нужны и у переменной, и у параметра, и при вызове.
  • Правило: много читателей или один писатель, но никогда вместе.
  • Заимствование заканчивается на последнем использовании ссылки, поэтому ошибку заимствования часто исправляет перестановка кода.
  • Всё это проверяется при компиляции, поэтому во время работы ничего не стоит.

Проверь себя

1. Что делает & перед значением, когда его передают в функцию?

2. Какое сочетание правила заимствования разрешают одновременно?

3. Что нужно, чтобы изменить значение через ссылку?

4. Почему Rust может следить за правилами заимствования без потери производительности во время работы?

Результат: 0 / 4