Что такое ownership и зачем он нужен
Ownership — та самая штука, из-за которой Rust не нужен ни ручной free, ни сборщик мусора.
Ownership (владение) — набор правил, по которым компилятор ещё до запуска программы решает, кто отвечает за освобождение каждого куска памяти и когда именно это произойдёт.
Вопрос: «Что такое владение в Rust и какую проблему оно решает?»
С этого начинается почти каждое собеседование. Формулировка бывает мягче — «расскажи своими словами про ownership» — но суть та же: интервьюер хочет услышать не определение из книги, а зачем язык вообще пошёл на такие сложности.
Что на самом деле проверяет интервьюер
Понимаешь ли ты, что ownership — это третий способ управлять памятью. Первый — ручной (C: malloc/free), второй — сборщик мусора (Java, Go, Python). Rust выбрал третий: правила освобождения памяти описаны в системе типов, и их проверяет компилятор. Кандидат уровня junior, который отвечает «ownership — это когда у значения один владелец» и замолкает, звучит как человек, прочитавший первую страницу. Нужно продолжение: и поэтому не бывает double free, use-after-free и утечек по забывчивости.
Три правила, которые надо помнить дословно
- У каждого значения в Rust есть переменная-владелец.
- Владелец в каждый момент времени ровно один.
- Когда владелец выходит из области видимости, значение освобождается.
Всё остальное в языке — move, borrow checker, lifetime — это следствия этих трёх строк.
Стек и куча: почему без этого не объяснить
Значения фиксированного размера (i32, bool, char) целиком лежат на стеке — их копирование бесплатно. String и Vec<T> устроены сложнее: на стеке лежит «заголовок» из трёх слов (указатель на буфер, длина, ёмкость), а сами байты — в куче. Именно за буфер в куче кто-то обязан отвечать, и этот кто-то — владелец.
fn main() {
let n = 42; // 4 байта целиком на стеке
let s = String::from("привет"); // заголовок на стеке, байты в куче
println!("{} {} ({} байт)", n, s, s.len());
}
Вывод:
42 привет (12 байт)
Двенадцать, а не шесть: len() у String считает байты UTF-8, а каждая кириллическая буква занимает два. Мелочь, но на собеседовании её любят спрашивать следом.
Вопрос: «Почему Rust безопасен по памяти без сборщика мусора?»
Что на самом деле проверяет интервьюер
Различаешь ли ты «безопасность во время выполнения» и «безопасность во время компиляции». В Java или Go за корректность платят рантаймом: GC ходит по объектам, тратит такты, иногда останавливает мир. В Rust за неё платят на этапе сборки — временем компилятора и временем программиста, который спорит с borrow checker. В готовом бинарнике никаких проверок владения не остаётся: это и называют «нулевой стоимостью абстракции».
| Подход | Кто освобождает память | Чем платим |
| Ручной (C, C++ без RAII) | Программист вызывает free | double free, use-after-free, утечки |
| Сборщик мусора (Java, Go) | Рантайм, когда сочтёт нужным | паузы, лишняя память, непредсказуемость |
| Ownership (Rust) | Компилятор вставляет вызов drop | сложность языка, борьба с borrow checker |
Обрати внимание: Rust не «не освобождает память» и не «делает это волшебно». Он делает ровно то же, что делал бы аккуратный C-программист, — просто места вызовов расставляет компилятор, а не человек.
Вопрос: «Что происходит с переменной, когда она выходит из области видимости?»
Вызывается drop. Если тип реализует трейт Drop, сначала выполняется его метод drop(&mut self), затем рекурсивно освобождаются поля. Это классический RAII: захват ресурса — это инициализация, освобождение привязано к концу жизни значения. Порядок — обратный порядку объявления, как на стеке.
struct Noisy(&'static str);
impl Drop for Noisy {
fn drop(&mut self) {
println!("освобождаю {}", self.0);
}
}
fn main() {
let _a = Noisy("внешний");
{
let _b = Noisy("внутренний");
println!("внутри блока");
}
println!("конец main");
}
Вывод:
внутри блока освобождаю внутренний конец main освобождаю внешний
_b умирает на закрывающей фигурной скобке, _a — в конце main. Никакого GC, никакой недетерминированности: момент освобождения известен по исходнику.
А если ссылка переживёт значение?
Тогда программа просто не соберётся — и это лучший вид ошибки. Вот код, который в C дал бы use-after-free:
fn main() {
let r;
{
let s = String::from("привет");
r = &s;
}
println!("{}", r);
}
Ошибка компиляции:
error[E0597]: `s` does not live long enough
--> src/main.rs:5:13
|
4 | let s = String::from("привет");
| - binding `s` declared here
5 | r = &s;
| ^^ borrowed value does not live long enough
6 | }
| - `s` dropped here while still borrowed
7 | println!("{}", r);
| - borrow later used here
Компилятор буквально показывает: вот здесь значение умерло, а вот здесь ты им пользуешься. Ровно этот сценарий в C++ дал бы висячий указатель и падение через полгода в проде.
Типичные ошибки кандидатов
- Говорить «в Rust нет утечек памяти». Утечки возможны:
Box::leak,std::mem::forget, циклы изRc. Ownership защищает от небезопасности, а не от глупости — утечка памяти считается безопасной операцией. - Путать ownership и borrow checker. Ownership — правила владения; borrow checker — часть компилятора, которая их проверяет вместе с правилами заимствования.
- Считать, что проверки происходят в рантайме. Нет: после компиляции от них не остаётся ни одной инструкции.
- Забывать про Drop и говорить «память просто освобождается».
Dropзакрывает не только память, но и файлы, сокеты, мьютексы. - Отвечать только про стек, ни разу не упомянув кучу — тогда непонятно, зачем вообще нужно владение.
Как ответить кратко
Ownership — это правила из трёх пунктов: у значения один владелец, владелец один в каждый момент времени, при выходе владельца из области видимости значение освобождается. Компилятор проверяет их статически и сам расставляет вызовы
drop, поэтому Rust получает безопасность по памяти без сборщика мусора и без ручногоfree.
- Проблема, которую решаем: double free, use-after-free, висячие указатели.
- Цена: платим временем компиляции и сложностью языка, а не рантаймом.
- Механика: RAII плюс трейт
Drop, порядок освобождения обратный объявлению.