владение сверх base-моделиownership beyond the base model

Умные указателиSmart pointers

Box, Rc, Arc, RefCell и Cell — пять типов стандартной библиотеки, дающие модели владения/заимствования то, чего не может дать компилятор один: единоличное владение на куче, разделяемое владение, и внутреннюю изменяемость с проверкой правил заимствования в рантайме вместо компиляции.Box, Rc, Arc, RefCell, and Cell — five standard-library types that give the ownership/borrowing model what the compiler alone can't: sole heap ownership, shared ownership, and interior mutability with borrow-rule checking moved to runtime instead of compile time.

память и конкурентностьmemory and concurrency частый вопрос: чем Rc отличается от Arc и зачем нужен RefCellcommon question: how Rc differs from Arc, and why RefCell exists

Что этоWhat it is

Box<T> — единоличное владение значением на куче, простейшая возможная аллокация, аналог std::unique_ptr. Rc<T> — разделяемое владение через подсчёт ссылок для однопоточного кода, аналог std::shared_ptr, но не потокобезопасный. Arc<T> — потокобезопасная версия Rc<T> с атомарным счётчиком, настоящий аналог shared_ptr в конкурентном контексте. RefCell<T> — "внутренняя изменяемость": обычные правила заимствования Rust переносятся с этапа компиляции на рантайм (нарушение — паника, а не отказ компилировать), позволяя изменять значение через разделяемую (&) ссылку в контролируемых случаях. Cell<T> — более простой примитив внутренней изменяемости только для Copy-типов: get/set без выдачи ссылки на содержимое вообще, так что проверять alias-правила не нужно даже в рантайме.Box<T> is sole ownership of a heap-allocated value — the simplest possible heap allocation, the analogue of std::unique_ptr. Rc<T> is reference-counted shared ownership for single-threaded code, the analogue of std::shared_ptr but not thread-safe. Arc<T> is the thread-safe version of Rc<T> with an atomic counter — the true analogue of shared_ptr in a concurrent context. RefCell<T> is "interior mutability": Rust's usual borrow rules move from compile time to runtime (a violation panics instead of failing to compile), letting code mutate a value through a shared (&) reference in controlled cases. Cell<T> is a simpler interior-mutability primitive for Copy types only — get/set with no reference to the contents ever handed out, so there's nothing to alias-check even at runtime.

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

Обычное владение и заимствование (см. ownership.html и borrowing-references.html) не может выразить каждый реальный паттерн владения данными, встречающийся на практике:Plain ownership and borrowing (see ownership.html and borrowing-references.html) can't express every real data-ownership pattern that comes up in practice:

01

Разделяемые и циклические структуры выходят за пределы статической проверкиShared and cyclic structures fall outside static checking

Дерево с обратными ссылками на родителя, граф, кэш, разделяемый между несколькими владельцами, — во всех этих случаях borrow checker, работающий чисто структурно и на этапе компиляции, не может статически доказать безопасность, потому что само понятие "ровно один владелец" здесь не подходит по смыслу задачи.A tree with parent back-references, a graph, a cache shared across multiple owners — in every one of these cases, the purely structural, compile-time-only borrow checker can't statically prove safety, because "exactly one owner" simply doesn't fit the shape of the problem.

02

Изменение одного поля при внешнем immutable-заимствовании всей структурыMutating one field while the struct is only immutably borrowed from outside

Иногда структуре нужно изменить одно внутреннее поле (например, счётчик обращений), оставаясь при этом доступной снаружи только через &self — этот паттерн тоже не укладывается в статическую модель "либо вся структура изменяема, либо нет".Sometimes a struct needs to mutate one internal field (e.g. an access counter) while remaining accessible from the outside only via &self — this pattern also doesn't fit the static "either the whole struct is mutable or it isn't" model.

03

Явные escape-hatch'и, а не обход безопасностиDeliberate escape hatches, not a bypass of safety

Эти пять типов не обходят гарантии безопасности Rust — они переносят ЧАСТЬ проверок с компиляции на рантайм (RefCell) или добавляют строго определённый безопасный протокол поверх разделения (атомарный/неатомарный подсчёт ссылок у Rc/Arc, простое единоличное владение у Box). Каждый — осознанный, безопасный escape hatch под конкретный паттерн, а не способ писать unsafe-код.These five types don't bypass Rust's safety guarantees — they move SOME of the checking from compile time to runtime (RefCell) or add a well-defined, safe protocol on top of raw sharing (Rc/Arc's atomic/non-atomic refcounting, Box's simple sole heap ownership). Each is a deliberate, safe escape hatch for a specific pattern, not a way to write unsafe code.

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

Классический мотивирующий пример для Box<T> — рекурсивный тип. Без boxing'а рекурсивный enum вроде связного списка имел бы бесконечный размер, потому что каждый вариант содержал бы себя же целиком:The classic motivating example for Box<T> is a recursive type. Without boxing, a recursive enum like a linked list would have an infinite size, because each variant would contain itself whole:

enum List {
    Cons(i32, Box<List>),  // Box даёт известный размер указателя вместо бесконечной вложенности// Box gives a known pointer-sized field instead of infinite nesting
    Nil,
}

Rc<T> добавляет разделяемое владение: .clone() лишь увеличивает счётчик ссылок — это дешёвая операция, инкремент целого числа, а НЕ глубокое копирование данных. Когда несколько владельцев должны ещё и изменять общие данные, чрезвычайно распространённая идиома — Rc<RefCell<T>>: .borrow_mut() выдаёт изменяемый доступ и паникует в рантайме при нарушении правил заимствования.Rc<T> adds shared ownership: .clone() only bumps the reference count — a cheap operation, an integer increment, NOT a deep copy of the data. When multiple owners also need to mutate the shared data, the extremely common idiom is Rc<RefCell<T>>: .borrow_mut() hands out mutable access and panics at runtime if borrow rules are violated.

use std::rc::Rc;
use std::cell::RefCell;

let shared = Rc::new(RefCell::new(0));
let handle = Rc::clone(&shared);  // счётчик ссылок 1 -> 2, данные не копируются// refcount 1 -> 2, no data is copied
*handle.borrow_mut() += 1;      // изменение через общую ссылку — проверка правил в рантайме// mutation through a shared reference — rules checked at runtime

Для кросс-поточного случая тот же паттерн выглядит как Arc<Mutex<T>> — потокобезопасный аналог Rc<RefCell<T>> (сам Mutex — тема std/, а не language/, здесь он упоминается только как концептуальный аналог, см. также mutex-condvar.html в C++).For the cross-thread case, the same pattern looks like Arc<Mutex<T>> — the thread-safe analogue of Rc<RefCell<T>> (Mutex itself is a std/ topic, not language/; it's mentioned here only as a conceptual analogue, see also mutex-condvar.html in C++).

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

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

Box для рекурсивного типа, который иначе не скомпилируетсяBox enabling a recursive type that otherwise won't compile

Связный список или узел бинарного дерева, определённые как рекурсивный enum, не имеют известного на этапе компиляции размера без boxing'а — компилятор сразу сообщит об "infinite size" типе. Box переносит содержимое на кучу, оставляя в enum лишь указатель фиксированного размера.A linked list or binary-tree node defined as a recursive enum has no compile-time-known size without boxing — the compiler immediately reports an "infinite size" type. Box moves the contents to the heap, leaving only a fixed-size pointer in the enum.

enum Tree {
    Node(i32, Box<Tree>, Box<Tree>),
    Leaf,
}
// без Box: рекурсия "внутри себя" -> бесконечный размер, ошибка компиляции// without Box: recursion "inside itself" -> infinite size, compile error
02

Rc для разделяемой read-only конфигурацииRc for a shared, read-only configuration object

Объект конфигурации, на который ссылаются несколько независимых частей однопоточной программы, — классический сценарий для Rc<T>. .clone() — это O(1) инкремент счётчика ссылок, а не глубокое копирование содержимого.A configuration object referenced by several independent parts of a single-threaded program is the classic Rc<T> scenario. .clone() is an O(1) refcount increment, not a deep copy of the contents.

let config = Rc::new(AppConfig { timeout_ms: 500 });
let for_worker = Rc::clone(&config);  // O(1), общий буфер с config// O(1), shares the same buffer as config
spawn_worker(for_worker);
03

Rc<RefCell<T>> для разделяемого, изменяемого узла графаRc<RefCell<T>> for a shared, mutable graph-node-like structure

Когда несколько владельцев узла графа должны иногда изменять общие данные, Rc<RefCell<T>> — стандартная идиома. Если код попытается удержать одновременно две изменяемых заимствования через два отдельных .borrow_mut(), программа паникует в рантайме с BorrowMutError.When several owners of a graph node occasionally need to mutate shared data, Rc<RefCell<T>> is the standard idiom. If code attempts to hold two mutable borrows simultaneously via two separate .borrow_mut() calls, the program panics at runtime with a BorrowMutError.

let node = Rc::new(RefCell::new(Vec::<i32>::new()));

let first_borrow = node.borrow_mut();
let second_borrow = node.borrow_mut();  // ПАНИКА в рантайме: already mutably borrowed// RUNTIME PANIC: already mutably borrowed
04

Cell для простого счётчика внутри immutable-заимствованной структурыCell for a simple counter field inside an immutably-borrowed struct

Счётчик попаданий в кэш, увеличиваемый внутри метода с &self, — типичный случай для Cell<T>: никакой рантайм-проверки заимствований не требуется вообще, потому что Cell никогда не выдаёт ссылку на содержимое — только get/set по значению.A cache-hit counter incremented inside a &self method is the typical Cell<T> case: no runtime borrow-checking machinery is needed at all, because Cell never hands out a reference to its contents — only get/set by value.

struct Cache { hits: std::cell::Cell<u32> }

impl Cache {
    fn record_hit(&self) {
        self.hits.set(self.hits.get() + 1);  // изменение через &self, без &mut self// mutation through &self, no &mut self needed
    }
}

ГраблиGotchas

RefCell превращает compile-time ошибку в runtime-паникуRefCell turns a compile-time error into a runtime panic

Нарушение правил заимствования, которое с обычными &/&mut было бы ошибкой КОМПИЛЯЦИИ, с RefCell<T> становится ПАНИКОЙ в рантайме — это обмен гарантии времени компиляции на гибкость, и код, активно полагающийся на RefCell, требует той же дисциплины, что и язык вообще без borrow checker'а, потому что компилятор больше не ловит эти ошибки заранее.A borrow-rule violation that would be a COMPILE error with plain &/&mut references becomes a runtime PANIC instead with RefCell<T> — this trades a compile-time guarantee for flexibility, and code relying heavily on RefCell needs the same discipline as a language with no borrow checker at all, since the compiler no longer catches these mistakes ahead of time.

Rc/Arc сами по себе не дают внутренней изменяемостиRc/Arc alone do not provide interior mutability

Rc<T> даёт нескольких владельцев неизменяемого значения; чтобы изменять разделяемые данные, почти всегда нужно сочетание с RefCell<T> (однопоточный случай: Rc<RefCell<T>>) или Mutex<T>/RwLock<T> (многопоточный: Arc<Mutex<T>>) — использование одного лишь Rc и удивление, почему изменение не работает, очень распространённая ранняя путаница.Rc<T> gives multiple owners of an immutable value; to mutate shared data you virtually always need to combine it with RefCell<T> (single-threaded: Rc<RefCell<T>>) or Mutex<T>/RwLock<T> (multi-threaded: Arc<Mutex<T>>) — using Rc alone and being surprised it doesn't allow mutation is a very common early confusion.

Rc/Arc могут создавать циклы, которые навсегда утекаютRc/Arc can create cycles that leak permanently

Если два узла, владеющих друг другом через Rc, ссылаются друг на друга сильной ссылкой, счётчик ни одного из них никогда не дойдёт до нуля — модель владения Rust предотвращает use-after-free и data race, но НЕ предотвращает утечки памяти в целом, и циклы Rc/Arc — стандартный тому пример. Решение — Weak<T>, невладеющая ссылка, которая не удерживает счётчик живым, специально созданная для обратных ссылок в циклических структурах вроде указателя на родителя в дереве.If two nodes each hold a strong Rc to the other, neither's refcount ever reaches zero — Rust's ownership model prevents use-after-free and data races but does NOT prevent memory leaks in general, and Rc/Arc cycles are the standard example. The fix is Weak<T>, a non-owning reference that doesn't keep the count alive, purpose-built for back-references in cyclic structures like a parent pointer in a tree.