Заимствование в 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 или в локальном проекте. Если компилятор жалуется, прочитай ошибку: обычно она точно показывает, где пересекаются заимствования.
1. Посчитать, не забирая
Заголовок раздела «1. Посчитать, не забирая»Напиши функцию 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() считает байты, а не буквы.
2. Список покупок
Заголовок раздела «2. Список покупок»Напиши две функции: 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 заканчивается, когда возвращается вызов, — поэтому следующий вызов может снова его одолжить.
3. Два писателя сразу
Заголовок раздела «3. Два писателя сразу»Эта программа не компилируется. Исправь её так, чтобы она выводила Счёт: 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 here4 | let second = &mut message; | ^^^^^^^^^^^^ second mutable borrow occurs here5 | 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
Что почитать
Заголовок раздела «Что почитать»- Ссылки и заимствование — книга «Язык программирования Rust»: официальная глава, включая висячие ссылки и то, почему Rust отказывается их создавать (перевод на русский).
- Заимствование — Rust на примерах: запускаемые примеры заимствований
&и продолжения про изменяемые заимствования и алиасинг (перевод на русский). - Non-lexical lifetimes — The Edition Guide (на английском): изменение, после которого заимствования заканчиваются на последнем использовании, а не на закрывающей скобке.
- Тип среза — книга «Язык программирования Rust»: как заимствовать только часть строки или массива — естественный следующий шаг после ссылок (перевод на русский).
reference— документация стандартной библиотеки (на английском): что такое&Tи&mut Tи какие трейты они реализуют.