сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Владение и лайфтаймы

Умные указатели в Rust: Box, Rc, Arc

Box, Rc и Arc

Умные указатели спрашивают, чтобы понять, умеешь ли ты выбирать способ владения под задачу. Box, Rc, Arc, Weak - каждый закрывает свой случай, и путать их дорого: лишний Arc платит атомарными операциями, а цикл из Rc течёт.

Стержень: Box - одно владение в куче, Rc и Arc - разделяемое владение через счётчик (неатомарный и атомарный), Weak - ссылка без владения.

// Формулировки: «зачем Box?», «чем Rc отличается от Arc?», «что будет с двумя Rc, ссылающимися друг на друга?»

Box: владение в куче

Box<T> - обычный указатель на значение в куче, владеющий и без накладных расходов сверх самого указателя. Нужен в трёх ситуациях: рекурсивный тип (список, дерево, иначе размер бесконечен), трейт-объект (Box<dyn Error>), большое значение, которое дорого таскать по стеку.

Освобождение обычное: когда Box выходит из области видимости, значение уничтожается. Никаких счётчиков и никакой магии - просто владение, перенесённое в кучу.

enum List {
    Cons(i32, Box<List>),
    Nil,
}
Box
владеющий указатель на значение в куче
рекурсивный тип
тип, содержащий сам себя; требует косвенности

Rc и Arc: разделяемое владение

Когда владельцев должно быть несколько, берут подсчёт ссылок. Rc считает их обычным числом - дёшево, но тип не Send, и компилятор не пустит его в другой поток. Arc делает то же атомарно и потому годится для многопотока, платя на каждом клоне и уничтожении.

Важное ограничение: ни тот, ни другой не дают изменяемости. Rc<T> отдаёт только &T. Чтобы менять содержимое, внутрь кладут RefCell (один поток) или Mutex/RwLock (несколько потоков) - отсюда типовые связки Rc<RefCell<T>> и Arc<Mutex<T>>.

// Проверено: попытка отправить Rc в поток даёт E0277 «Rc<i32> cannot be sent between threads safely» - компилятор ловит это на сборке.

let a = Rc::new(5);
let b = Rc::clone(&a);
Rc::strong_count(&a); // 2
Rc
подсчёт ссылок в одном потоке, неатомарный счётчик
Arc
то же с атомарным счётчиком, пригоден для потоков

Циклы и Weak

Подсчёт ссылок не умеет разрывать циклы. Два узла, ссылающиеся друг на друга через Rc, держат друг друга: при выходе из области видимости счётчики падают с двух до единицы и там застревают. Память не освобождается, деструкторы не вызываются - утечка, которая, с точки зрения языка, совершенно безопасна.

Лечение - Weak: слабая ссылка не входит в счётчик владения. Типовая схема для дерева: родитель держит детей через Rc, ребёнок смотрит на родителя через Weak. Достучаться до значения можно через upgrade(), который вернёт Some, пока объект жив, и None после его смерти.

// Проверено: в цикле из двух Rc деструкторы узлов не вызываются вовсе, а upgrade у Weak после drop владельца честно отдаёт None.

Weak
слабая ссылка без владения; upgrade даёт Option
цикл ссылок
взаимные Rc, из-за которых счётчик не обнуляется

Как отвечать: «Чем Rc отличается от Arc и когда что брать?»

Функционально это одно и то же - разделяемое владение через подсчёт ссылок. Разница в цене синхронизации: у Rc счётчик обычный, поэтому он дешевле, но тип не Send и в другой поток его не отдать - компилятор поймает это на сборке. У Arc счётчик атомарный, он годится для многопотока и платит атомарной операцией на каждом клоне и уничтожении. Беру Rc по умолчанию в однопоточном коде, Arc - когда значение реально уезжает в потоки. И ни тот, ни другой не дают изменяемости: для неё внутрь кладут RefCell или Mutex.

Ответ показывает и разницу в реализации, и практическое правило выбора, и понимание, что оба типа не про мутабельность это частая путаница.

На чём валятся

  • Ждут, что Rc или Arc дадут изменяемый доступ. Оба отдают только &.
  • Берут Arc в однопоточном коде и платят атомарными операциями без нужды.
  • Не знают про циклы Rc и уверяют, что утечки в Rust невозможны.
  • Путают Weak с обычной ссылкой: upgrade возвращает Option, и это важно.
  • Не могут объяснить, почему Rc не Send, а именно этим вопросом обычно и добивают.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.

  1. #rs_smart_pointers1 / 5
    Чем Rc<T> отличается от Arc<T>?
    A)Rc считает ссылки обычным счётчиком, Arc — атомарным для потоков
    B)Rc живёт на стеке, Arc — в куче
    C)Rc освобождает память сразу, Arc откладывает до сборки мусора
    D)Rc для неизменяемых данных, Arc разрешает менять значение внутри
    показать ответ и разбор
    +A)Rc считает ссылки обычным счётчиком, Arc — атомарным для потоков

    // разбор: Функционально это один и тот же подсчёт ссылок, разница в цене синхронизации. У Rc счётчик неатомарный и потому дешёвый, но тип не Send — компилятор не пустит его в другой поток. Arc платит атомарными операциями и потому пригоден для многопотока. Мутабельности не даёт ни тот, ни другой: нужен RefCell или Mutex внутри.

  2. #rs_smart_pointers2 / 5
    Зачем в однопоточном коде связка Rc<RefCell<T>>?
    A)Rc хранит данные, RefCell кэширует их копию для быстрого чтения
    B)Rc даёт несколько владельцев, RefCell — правку через &
    C)Rc защищает от гонок, RefCell от паник при обращении
    D)Такая связка нужна, чтобы значение пережило текущую функцию
    показать ответ и разбор
    +B)Rc даёт несколько владельцев, RefCell — правку через &

    // разбор: Rc позволяет нескольким местам владеть значением, но отдаёт только & — менять нельзя. RefCell добавляет внутреннюю изменяемость: правило XOR проверяется в рантайме, а не компилятором. Вместе получается разделяемое изменяемое состояние в одном потоке — типовая основа графов и деревьев с обратными ссылками.

  3. #rs_smart_pointers3 / 5
    Два узла ссылаются друг на друга через Rc. Что произойдёт при выходе из области видимости?
    A)Сборщик циклов обнаружит их и освободит память
    B)Компилятор запретит такую конструкцию на этапе сборки
    C)Память освободится: Rc отслеживает циклы через счётчик слабых ссылок
    D)Счётчики не дойдут до нуля, деструкторы не вызовутся — утечка
    показать ответ и разбор
    +D)Счётчики не дойдут до нуля, деструкторы не вызовутся — утечка

    // разбор: Взаимные Rc держат друг друга: после выхода из области видимости счётчики падают с двух до единицы и там застревают. Память не освобождается, Drop не вызывается — классическая утечка, и она безопасна с точки зрения языка. Лечится Weak: обратную ссылку делают слабой, она в счётчик владения не входит.

  4. #rs_smart_pointers4 / 5
    Что вернёт Weak::upgrade(), если сильных ссылок на значение уже не осталось?
    A)Rc с новым счётчиком: upgrade воскрешает значение
    B)Ссылку на освобождённую память — отсюда unsafe в сигнатуре
    C)None: данные освобождены, слабая ссылка знает об этом
    D)Панику: обращение к мёртвому значению считается ошибкой
    показать ответ и разбор
    +C)None: данные освобождены, слабая ссылка знает об этом

    // разбор: Weak не удерживает данные: при обнулении сильного счётчика значение уничтожается, а служебный блок с счётчиками живёт, пока есть слабые ссылки. Поэтому upgrade возвращает Option — Some, пока значение живо, и None после. Так делают кэши и обратные ссылки «ребёнок → родитель», не создавая циклов.

  5. #rs_smart_pointers5 / 5
    Что напечатает код?
    let a = Rc::new(String::from("x"));
    println!("{}", Rc::strong_count(&a));
    {
        let _b = Rc::clone(&a);
        println!("{}", Rc::strong_count(&a));
    }
    println!("{}", Rc::strong_count(&a));
    A)1, затем 1, затем 1
    B)1, затем 2, затем 1
    C)1, затем 2, затем 2
    D)2, затем 3, затем 2
    показать ответ и разбор
    +B)1, затем 2, затем 1

    // разбор: Rc::clone не копирует строку, а увеличивает счётчик сильных ссылок: внутри блока владельцев двое. На выходе из блока _b уничтожается, счётчик падает обратно до единицы, и значение живо, пока он не дошёл до нуля. Отсюда и цена приёма: сам клон дешёвый, но каждый требует парного drop, а взаимные ссылки счётчик до нуля не доводят вовсе.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.