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

Владение в Rust

Владение (ownership) — это идея, которая делает Rust Rust. Благодаря ей язык гарантирует безопасную работу с памятью без сборщика мусора и без ручного освобождения памяти. Сначала это кажется непривычным, но суть простая. Давай разберёмся шаг за шагом.

Когда программа работает, она хранит данные в памяти. В какой-то момент эту память нужно освободить, иначе программа начнёт «течь» и в конце концов упадёт. Обычно языки решают это одним из двух способов:

  • Вручную (C/C++): ты сам освобождаешь память. Забыл — утечка. Освободил слишком рано — падения и дыры в безопасности.
  • Сборщик мусора (Java, Go, Python): фоновый процесс убирает за тебя. Удобно, но это стоит производительности и предсказуемости.

Rust идёт третьим путём: он ещё при компиляции точно решает, когда нужно убрать каждое значение, по набору правил владения. Никакого ручного освобождения и никакого сборщика мусора.

Это вся система. Всё остальное следует из них:

  1. У каждого значения есть один владелец (переменная, которая им «владеет»).
  2. В каждый момент владелец только один.
  3. Когда владелец выходит из области видимости, значение автоматически удаляется.

Посмотрим на каждое правило в деле.

Правило 3: значения удаляются в конце области видимости

Заголовок раздела «Правило 3: значения удаляются в конце области видимости»

Область видимости (scope) — это участок между { и }. Когда область видимости переменной заканчивается, Rust автоматически освобождает её значение — тебе никогда не нужно писать free():

fn main() {
{
let message = String::from("привет"); // здесь message действительна
println!("{message}");
} // <- область видимости закончилась, `message` автоматически удалена
// println!("{message}"); // ❌ ошибка: message здесь уже не существует
}
▶ Попробуй сам

Тебе ничего не пришлось освобождать — Rust сделал это в тот момент, когда message вышла из области видимости.

Вот тут становится интересно. Посмотри, что происходит, когда одну переменную присваивают другой:

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 или в локальном проекте. Ошибки компиляции по дороге — это нормально: читай их, обычно они указывают прямо на перемещение.

Эта программа не компилируется. Исправь её так, чтобы она выводила черновик -> черновик 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` trait
3 | let mut edited = original;
| -------- value moved here
4 | 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}");
}

Вывод:

черновик -> черновик v2
▶ Попробуй сам

let mut edited = original; перемещает строку, и original больше недействительна. .clone() создаёт вторую строку со своей памятью. Теперь push_str меняет только edited, а в original по-прежнему черновик.

Напиши функцию 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 (затеняя старую). Это работает, но всё время передавать значения туда и обратно утомительно. Именно эту проблему решает заимствование.

Эта программа не компилируется. Исправь её так, чтобы она выводила Аня в списке, а затем ["Аня"].

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` trait
4 | names.push(name);
| ---- value moved here
5 | 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