Владение в Rust
Владение (ownership) — это идея, которая делает Rust Rust. Благодаря ей язык гарантирует безопасную работу с памятью без сборщика мусора и без ручного освобождения памяти. Сначала это кажется непривычным, но суть простая. Давай разберёмся шаг за шагом.
Какую проблему решает владение
Заголовок раздела «Какую проблему решает владение»Когда программа работает, она хранит данные в памяти. В какой-то момент эту память нужно освободить, иначе программа начнёт «течь» и в конце концов упадёт. Обычно языки решают это одним из двух способов:
- Вручную (C/C++): ты сам освобождаешь память. Забыл — утечка. Освободил слишком рано — падения и дыры в безопасности.
- Сборщик мусора (Java, Go, Python): фоновый процесс убирает за тебя. Удобно, но это стоит производительности и предсказуемости.
Rust идёт третьим путём: он ещё при компиляции точно решает, когда нужно убрать каждое значение, по набору правил владения. Никакого ручного освобождения и никакого сборщика мусора.
Три правила владения
Заголовок раздела «Три правила владения»Это вся система. Всё остальное следует из них:
- У каждого значения есть один владелец (переменная, которая им «владеет»).
- В каждый момент владелец только один.
- Когда владелец выходит из области видимости, значение автоматически удаляется.
Посмотрим на каждое правило в деле.
Правило 3: значения удаляются в конце области видимости
Заголовок раздела «Правило 3: значения удаляются в конце области видимости»Область видимости (scope) — это участок между { и }. Когда область видимости переменной заканчивается, Rust автоматически освобождает её значение — тебе никогда не нужно писать free():
fn main() { { let message = String::from("привет"); // здесь message действительна println!("{message}"); } // <- область видимости закончилась, `message` автоматически удалена
// println!("{message}"); // ❌ ошибка: message здесь уже не существует}Тебе ничего не пришлось освобождать — Rust сделал это в тот момент, когда message вышла из области видимости.
Правила 1 и 2: «перемещение»
Заголовок раздела «Правила 1 и 2: «перемещение»»Вот тут становится интересно. Посмотри, что происходит, когда одну переменную присваивают другой:
fn main() { let s1 = String::from("привет"); let s2 = s1; // владение ПЕРЕМЕЩАЕТСЯ из s1 в s2
println!("{s2}"); // ✅ работает // println!("{s1}"); // ❌ ошибка: s1 перемещена}Когда ты пишешь let s2 = s1, Rust не копирует строку — он перемещает (move) владение из s1 в s2. Правило 2 говорит, что владелец может быть только один, поэтому s1 теперь недействительна. Попытка ею воспользоваться даёт:
error[E0382]: borrow of moved value: `s1`Это значит «заимствование перемещённого значения s1»: ты пытаешься прочитать переменную, из которой значение уже ушло.
А если мне нужны две копии?
Заголовок раздела «А если мне нужны две копии?»Если тебе действительно нужна вторая, независимая копия, вызови .clone():
fn main() { let s1 = String::from("привет"); let s2 = s1.clone(); // делает полную копию
println!("s1 = {s1}, s2 = {s2}"); // ✅ теперь работают обе}clone() сделан явным специально: копирование может быть дорогим, поэтому Rust заставляет тебя попросить о нём.
Простые типы копируются, а не перемещаются
Заголовок раздела «Простые типы копируются, а не перемещаются»Может возникнуть вопрос, почему вот это не вызывает ошибку:
fn main() { let x = 5; let y = x; println!("x = {x}, y = {y}"); // ✅ работают обе}Маленькие значения фиксированного размера — целые числа, числа с плавающей точкой, логические значения и char — копируются дёшево, поэтому Rust просто копирует их, а не перемещает. Для новичка правило такое:
- Простые значения (числа,
bool,char) → копируются. Исходная переменная остаётся действительной. - Данные с владельцем (например,
String) → перемещаются. Исходная переменная становится недействительной.
Владение и функции
Заголовок раздела «Владение и функции»Передача значения в функцию тоже его перемещает — так же, как присваивание переменной:
fn take_ownership(s: String) { println!("Теперь я владею: {s}");} // здесь s удаляется
fn main() { let text = String::from("привет"); take_ownership(text); // println!("{text}"); // ❌ ошибка: text перемещена в функцию}Именно поэтому существует следующая тема — заимствование. Оно позволяет функции пользоваться значением, не забирая владение, так что после вызова ты можешь продолжать им пользоваться.
Практика
Заголовок раздела «Практика»Сначала попробуй решить каждую задачу сам — в Rust Playground или в локальном проекте. Ошибки компиляции по дороге — это нормально: читай их, обычно они указывают прямо на перемещение.
1. Сохрани оригинал
Заголовок раздела «1. Сохрани оригинал»Эта программа не компилируется. Исправь её так, чтобы она выводила черновик -> черновик v2, не удаляя ни одну из переменных.
fn main() { let original = String::from("черновик"); let mut edited = original; edited.push_str(" v2"); println!("{original} -> {edited}");}error[E0382]: borrow of moved value: `original` --> src/main.rs:5:16 |2 | let original = String::from("черновик"); | -------- move occurs because `original` has type `String`, which does not implement the `Copy` trait3 | let mut edited = original; | -------- value moved here4 | edited.push_str(" v2");5 | println!("{original} -> {edited}"); | ^^^^^^^^ value borrowed here after moveКомпилятор показывает всю историю: в строке 3 значение переместилось («value moved here»), а в строке 5 ты пытаешься им воспользоваться («value borrowed here after move»). Тип String не реализует трейт Copy, поэтому он перемещается, а не копируется.
Подсказка: тебе нужны две независимые строки, чтобы изменение одной не затрагивало другую.
Показать решение
fn main() { let original = String::from("черновик"); let mut edited = original.clone(); // полная отдельная копия edited.push_str(" v2"); println!("{original} -> {edited}");}Вывод:
черновик -> черновик v2let mut edited = original; перемещает строку, и original больше недействительна. .clone() создаёт вторую строку со своей памятью. Теперь push_str меняет только edited, а в original по-прежнему черновик.
2. Верни обратно
Заголовок раздела «2. Верни обратно»Напиши функцию add_signature, которая забирает владение строкой String, добавляет в конец " -- Феррис" и возвращает строку обратно. Вызови её из main и выведи результат.
Подсказка: функция может вернуть значение, которое ей передали. Напиши -> String в сигнатуре и прими параметр как mut text: String, чтобы его можно было менять.
Показать решение
fn add_signature(mut text: String) -> String { text.push_str(" -- Феррис"); text // владение перемещается обратно к вызывающему коду}
fn main() { let letter = String::from("До скорой встречи!"); let letter = add_signature(letter); println!("{letter}");}Вывод:
До скорой встречи! -- ФеррисПри вызове владение перемещается в функцию, а с возвращаемым значением — обратно. Мы ловим его в новую переменную letter (затеняя старую). Это работает, но всё время передавать значения туда и обратно утомительно. Именно эту проблему решает заимствование.
3. Кто теперь владелец?
Заголовок раздела «3. Кто теперь владелец?»Эта программа не компилируется. Исправь её так, чтобы она выводила Аня в списке, а затем ["Аня"].
fn main() { let mut names = Vec::new(); let name = String::from("Аня"); names.push(name); println!("{name} в списке"); println!("{:?}", names);}error[E0382]: borrow of moved value: `name` --> src/main.rs:5:16 |3 | let name = String::from("Аня"); | ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait4 | names.push(name); | ---- value moved here5 | println!("{name} в списке"); | ^^^^ value borrowed here after moveТа же ошибка: в строке 4 значение переместилось в вектор, а в строке 5 ты снова обращаешься к name.
Подсказка: push забирает владение, как и вызов функции. Кто владеет строкой после строки 4? Либо используй name до перемещения, либо спроси нового владельца.
Показать решение
fn main() { let mut names = Vec::new(); let name = String::from("Аня"); println!("{name} в списке"); // используем, пока ещё владеем names.push(name); // теперь владелец — вектор println!("{:?}", names);}Вывод:
Аня в списке["Аня"]После names.push(name) владельцем становится вектор, и name больше использовать нельзя. Вывести строку заранее — одно из решений. Другое — оставить исходный порядок и вывести names[0], ведь строка по-прежнему лежит в векторе. Клонирование тоже сработает, но копировать здесь незачем.
- Владение позволяет Rust безопасно управлять памятью без сборщика мусора.
- Три правила: у значения один владелец, в каждый момент владелец только один, значение удаляется, когда владелец выходит из области видимости.
- Присваивание данных с владельцем (например,
String) их перемещает — исходная переменная становится недействительной. - Используй
.clone()для независимой копии; простые типы (числа,bool,char) копируются автоматически. - Передача значения в функцию тоже его перемещает — и это подводит нас к заимствованию.
Проверь себя
Заголовок раздела «Проверь себя»Проверь себя
1. Сколько владельцев может быть у значения в Rust в один момент времени?
2. Какой метод нужно вызвать, чтобы сделать независимую копию String, а не переместить её?
3. Какой из этих типов при присваивании другой переменной автоматически копируется, а не перемещается?
4. Что происходит, когда ты передаёшь String в функцию, которая принимает её по значению (не по ссылке)?
5. Когда, согласно этой странице, Rust автоматически удаляет (drop) значение?
Результат: 0 / 5
Что почитать
Заголовок раздела «Что почитать»- Что такое «владение»? — книга «Язык программирования Rust»: официальная глава с картинками того, как
Stringвыглядит в памяти и почему перемещение её не копирует (перевод на русский). - Владение и перемещение — Rust на примерах: короткие запускаемые примеры перемещений, в том числе в функции (перевод на русский).
- RAII — Rust на примерах: как значения удаляются при выходе из области видимости и при чём тут трейт
Drop(перевод на русский). Copy— документация стандартной библиотеки (на английском): какие типы копируются, а не перемещаются, и чемCopyотличается отClone.- Rust track — Exercism (на английском): бесплатные задачи с обратной связью, когда захочется больше упражнений, чем есть на этой странице.