фундамент модели памятиthe memory model's foundation

Ownership

У каждого значения в Rust ровно один владелец; когда владелец выходит из области видимости, значение уничтожается. Это правило, проверяемое компилятором на этапе компиляции без сборщика мусора и без ручного free(), — центральная идея, вокруг которой построен весь язык.Every value in Rust has exactly one owner; when the owner goes out of scope, the value is dropped. This rule, checked by the compiler at compile time with no garbage collector and no manual free(), is the central idea the whole language is built around.

основа языкаcore of the language частый вопрос: что такое ownership и зачем он нужен вместо GCcommon question: what is ownership and why it exists instead of a GC

Что этоWhat it is

Ownership — набор из трёх правил, проверяемых компилятором: (1) у каждого значения есть переменная-владелец; (2) владелец в любой момент времени ровно один; (3) когда владелец выходит из области видимости, значение уничтожается (вызывается Drop::drop, если он реализован). Присваивание владеющего значения (не ссылки) другой переменной — это move: старая переменная становится недействительной, компилятор запрещает её дальнейшее использование.Ownership is a set of three rules checked by the compiler: (1) every value has an owning variable; (2) there is exactly one owner at any given time; (3) when the owner goes out of scope, the value is dropped (Drop::drop runs, if implemented). Assigning an owning value (not a reference) to another variable is a move: the old variable becomes invalid, and the compiler forbids using it afterward.

C++ — владение по соглашению, не по правилам языкаC++ — ownership by convention, not by language rule
std::vector<int> a = {1, 2, 3};
std::vector<int> b = a;  // глубокая копия — легально, но может быть случайной// a deep copy — legal, but can happen by accident
a.push_back(4);         // a по-прежнему валиден, компилятор не проверяет "кто владеет"// a is still valid — the compiler tracks no notion of "who owns this"
Rust — владение как часть системы типовRust — ownership as part of the type system
let a = vec![1, 2, 3];
let b = a;              // move — владение переходит к b, не копия// a move — ownership transfers to b, not a copy
a.push(4);            // ОШИБКА КОМПИЛЯЦИИ: use of moved value: `a`// COMPILE ERROR: use of moved value: `a`

Зачем ввелиWhy it was introduced

Ownership решает задачу управления памятью, для которой раньше существовало только два неудовлетворительных ответа:Ownership solves a memory-management problem that previously had only two unsatisfying answers:

01

Безопасность памяти без сборщика мусораMemory safety without a garbage collector

Языки с GC (Java, Go, Python) избегают use-after-free и double-free ценой рантайм-накладных расходов (паузы на сборку мусора, непредсказуемая задержка) — неприемлемо для системного и low-latency кода. Ownership даёт те же гарантии безопасности памяти статически, на этапе компиляции, без единого байта рантайм-оверхеда — компилятор просто отказывается собирать код, который мог бы обратиться к уже освобождённой памяти.GC languages (Java, Go, Python) avoid use-after-free and double-free at the cost of runtime overhead (GC pauses, unpredictable latency) — unacceptable for systems and low-latency code. Ownership gives the same memory-safety guarantees statically, at compile time, with zero bytes of runtime overhead — the compiler simply refuses to build code that could touch already-freed memory.

02

C/C++ полагались на дисциплину программиста, а не на компиляторC/C++ relied on programmer discipline, not the compiler

В C++ RAII (unique_ptr) выражает "владение" соглашением, но ничто не мешает случайно скопировать сырой указатель, забыть, что объект уже перемещён (moved-from состояние остаётся валидным для использования — просто "неопределённым", а не запрещённым), или удалить объект дважды. Каждый такой класс багов (use-after-free, double-free, dangling pointer) годами оставался источником реальных уязвимостей (CVE) в крупных C/C++ проектах.In C++, RAII (unique_ptr) expresses "ownership" as a convention, but nothing stops you from accidentally copying a raw pointer, forgetting an object was already moved-from (a moved-from state remains legal to use — just "unspecified", not forbidden), or deleting an object twice. Each such bug class (use-after-free, double-free, dangling pointer) has for years been a real source of CVEs in large C/C++ codebases.

03

Move как язык по умолчанию, а не опциональная оптимизацияMove as the default, not an optional optimization

В C++11 move-семантика (move-semantics.html) — это opt-in оптимизация поверх системы, где копирование остаётся дефолтом. В Rust всё наоборот: move — поведение по умолчанию для владеющих типов (без реализации Copy), а явное копирование требует явного .clone() — эта инверсия дефолта заставляет разработчика осознанно решать, где действительно нужна копия, а не полагаться на то, что компилятор "оптимизирует лишнее копирование" постфактум.In C++11, move semantics (move-semantics.html) are an opt-in optimization on top of a system where copying remains the default. In Rust it's the reverse: move is the default behavior for owning types (without a Copy implementation), and explicit copying requires an explicit .clone() — this inversion of the default forces the developer to consciously decide where a copy is actually needed, instead of relying on the compiler to "optimize away the extra copy" after the fact.

Как это работает под капотомHow it works under the hood

Move в Rust — это чисто статический, compile-time механизм: физически при перемещении String или Vec<T> происходит побитовое копирование их внутренней структуры (указатель на буфер, длина, capacity) в новую переменную — ровно как memcpy. Разница со случайным дублированием в том, что компилятор помечает старую переменную как "перемещённую" в своей внутренней модели потока данных (borrow checker) и статически запрещает любое обращение к ней дальше по коду — независимо от того, идёт ли речь об одном пути исполнения или о ветвлении.A move in Rust is a purely static, compile-time mechanism: physically, moving a String or Vec<T> performs a bitwise copy of their internal structure (a pointer to the buffer, length, capacity) into the new variable — exactly like memcpy. The difference from accidental duplication is that the compiler marks the old variable as "moved" in its internal data-flow model (the borrow checker) and statically forbids any further access to it — regardless of whether that's a single execution path or a branch.

fn takes_ownership(s: String) {
    println!("{}", s);
} // s выходит из области видимости здесь — вызывается Drop, буфер освобождается// s goes out of scope here — Drop runs, the buffer is freed

fn main() {
    let s = String::from("hello");
    takes_ownership(s);   // s перемещается в функцию — владение передано// s is moved into the function — ownership transferred
    // println!("{}", s);  // ОШИБКА: s уже перемещён и недействителен здесь// println!("{}", s);  // ERROR: s was already moved and is invalid here
}

Типы, реализующие маркерный трейт Copy (все примитивы вроде i32, bool, char, а также кортежи/массивы из них) — исключение: присваивание такого значения не move, а обычное побитовое копирование, и старая переменная остаётся полностью валидной. Copy разрешён компилятором только для типов, у которых нет собственного Drop и все поля сами Copy — потому что копирование значения с нетривиальной логикой освобождения ресурса (например, String, владеющего кучей) привело бы к double-free при двух независимых Drop на общий буфер.Types implementing the marker trait Copy (all primitives like i32, bool, char, plus tuples/arrays of them) are the exception: assigning such a value is not a move but an ordinary bitwise copy, and the old variable remains fully valid. The compiler only allows Copy for types with no custom Drop and whose fields are all themselves Copy — because copying a value with nontrivial resource-release logic (e.g. String, which owns heap memory) would cause a double-free from two independent Drop calls on the same buffer.

let x: i32 = 5;
let y = x;              // i32 реализует Copy — это копия, не move// i32 implements Copy — this is a copy, not a move
println!("{} {}", x, y);  // OK: x всё ещё валиден// OK: x is still valid

Примеры из реального кодаReal-world examples

типичные сценарии использованияtypical usage scenarios
01

Builder-паттерн, потребляющий self по значениюA builder pattern consuming self by value

Методы билдера принимают self (не &mut self) и возвращают его обратно — ownership гарантирует, что промежуточный билдер нельзя использовать повторно после вызова метода, что делает цепочку вызовов однозначной и предотвращает случайное переиспользование частично сконфигурированного состояния.Builder methods take self (not &mut self) and return it back — ownership guarantees an intermediate builder can't be reused after a method call, making the call chain unambiguous and preventing accidental reuse of a partially-configured state.

struct RequestBuilder { url: String, timeout_ms: u32 }

impl RequestBuilder {
    fn timeout(mut self, ms: u32) -> Self {
        self.timeout_ms = ms;
        self  // владение возвращается вызывающему// ownership is returned to the caller
    }
}
// builder.timeout(500) — старый builder больше недоступен, только новый self// builder.timeout(500) — the old builder is gone, only the new self remains
02

Передача владения в поток вместо разделяемой памятиTransferring ownership into a thread instead of sharing memory

thread::spawn с move-замыканием передаёт владение данными новому потоку — компилятор статически гарантирует, что породивший поток больше не сможет обратиться к данным, устраняя целый класс data race на уровне типов, без единого мьютекса.thread::spawn with a move closure transfers ownership of the data to the new thread — the compiler statically guarantees the spawning thread can no longer touch that data, eliminating a whole class of data races at the type level, with no mutex involved.

let data = vec![1, 2, 3];
let handle = thread::spawn(move || {
    println!("{:?}", data);  // data перемещена в поток// data has been moved into the thread
});
handle.join().unwrap();
03

RAII-освобождение ресурса ровно один раз через DropRAII resource release exactly once via Drop

Обёртка над файловым дескриптором или сетевым соединением реализует Drop — единственность владения гарантирует, что ресурс закроется ровно один раз, ровно тогда, когда владелец выходит из области видимости, без ручного вызова close() и без риска double-close.A wrapper around a file descriptor or network connection implements Drop — single ownership guarantees the resource closes exactly once, exactly when the owner goes out of scope, with no manual close() call and no risk of a double-close.

struct ConnectionGuard { fd: i32 }

impl Drop for ConnectionGuard {
    fn drop(&mut self) {
        close_fd(self.fd);  // вызывается ровно один раз — владелец только один// runs exactly once — there is only ever one owner
    }
}
04

Явное клонирование там, где нужна независимая копияExplicit cloning where an independent copy is actually needed

Когда двум структурам данных действительно нужны независимые копии одних и тех же данных (не разделяемый доступ, а именно две отдельные версии) — вызывается .clone() явно, что делает точку, где происходит потенциально дорогое копирование, видимой в коде, а не скрытой внутри конструктора копирования.When two data structures genuinely need independent copies of the same data (not shared access, but two truly separate versions) — .clone() is called explicitly, making the point where a potentially expensive copy happens visible in the code, rather than hidden inside a copy constructor.

let original = vec![1, 2, 3];
let snapshot = original.clone();  // явная, видимая глубокая копия// an explicit, visible deep copy
modify(original);                // snapshot остаётся неизменным независимо от modify// snapshot remains unaffected by modify

ГраблиGotchas

Частичный move из структуры делает недействительной только использованное полеA partial move out of a struct invalidates only the field that was moved

Если из структуры перемещено одно поле (например, let name = person.name;), сама структура person целиком становится частично недействительной — обращаться к person.name напрямую больше нельзя, но к остальным полям (person.age) можно, а вот использовать person целиком (например, передать в функцию по значению) уже нельзя. Эта частичность нередко удивляет новичков, ожидающих либо "всё еще валидно", либо "всё невалидно".If one field is moved out of a struct (e.g. let name = person.name;), the struct person as a whole becomes partially invalid — accessing person.name directly is no longer allowed, but other fields (person.age) still are, while using person as a whole (e.g. passing it by value to a function) is not. This partiality often surprises newcomers expecting either "still fully valid" or "fully invalid".

Move — не всегда физическое копирование под капотомA move isn't always a physical copy under the hood

Компилятор часто оптимизирует move так, что физического копирования байтов вообще не происходит (например, при возврате значения из функции — return value optimization/NRVO-подобные оптимизации применяются и здесь) — "move" в первую очередь статическая, типовая концепция ("кто теперь ответственен за это значение"), а не гарантированная runtime-операция определённой стоимости.The compiler frequently optimizes a move so that no physical byte-copying happens at all (e.g. when returning a value from a function — RVO/NRVO-like optimizations apply here too) — a "move" is primarily a static, type-level concept ("who is now responsible for this value"), not a guaranteed runtime operation of a specific cost.

Ownership не запрещает разделяемый доступ — для этого есть Rc/ArcOwnership doesn't forbid shared access — that's what Rc/Arc are for

"Ровно один владелец" — правило по умолчанию, а не абсолютный запрет разделять данные. Когда разделяемое владение действительно нужно (граф, дерево с обратными ссылками, кэш), для этого существуют Rc<T>/Arc<T> (см. smart-pointers.html) — они не нарушают модель ownership, а явно инкапсулируют подсчёт ссылок как отдельный, видимый в типе механизм."Exactly one owner" is the default rule, not an absolute ban on sharing data. When shared ownership is genuinely needed (a graph, a tree with back-references, a cache), Rc<T>/Arc<T> exist for exactly that (see smart-pointers.html) — they don't violate the ownership model, they explicitly encapsulate reference counting as a separate mechanism visible in the type.