Что такое ownership и зачем он нужен

Ownership — та самая штука, из-за которой Rust не нужен ни ручной free, ни сборщик мусора.

Ownership (владение) — набор правил, по которым компилятор ещё до запуска программы решает, кто отвечает за освобождение каждого куска памяти и когда именно это произойдёт.

Вопрос: «Что такое владение в Rust и какую проблему оно решает?»

С этого начинается почти каждое собеседование. Формулировка бывает мягче — «расскажи своими словами про ownership» — но суть та же: интервьюер хочет услышать не определение из книги, а зачем язык вообще пошёл на такие сложности.

Что на самом деле проверяет интервьюер

Понимаешь ли ты, что ownership — это третий способ управлять памятью. Первый — ручной (C: malloc/free), второй — сборщик мусора (Java, Go, Python). Rust выбрал третий: правила освобождения памяти описаны в системе типов, и их проверяет компилятор. Кандидат уровня junior, который отвечает «ownership — это когда у значения один владелец» и замолкает, звучит как человек, прочитавший первую страницу. Нужно продолжение: и поэтому не бывает double free, use-after-free и утечек по забывчивости.

Три правила, которые надо помнить дословно

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

Всё остальное в языке — 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)Программист вызывает freedouble 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, порядок освобождения обратный объявлению.
Проверьте себя
1. Сколько владельцев может быть у значения в Rust в один момент времени?
AСколько угодно, пока никто не изменяет значение
BРовно один
CОдин на поток выполнения
DНи одного, пока значение не помещено в Box
2. Когда в Rust вызывается drop для значения?
AПри очередном проходе сборщика мусора
BТолько если программист явно вызвал std::mem::drop
CКогда владелец выходит из области видимости
DВ момент, когда счётчик ссылок обнулился
3. Почему говорят, что ownership в Rust имеет нулевую стоимость в рантайме?
AПроверки владения выполняются на этапе компиляции и не попадают в бинарник
BRust вообще не освобождает память до завершения процесса
CОсвобождением занимается фоновый поток с низким приоритетом
DПамять выделяется только на стеке, куча не используется