Умные указатели в Rust: 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, остальные разбираются в тренажёре.
- Чем Rc<T> отличается от Arc<T>?A)Rc считает ссылки обычным счётчиком, Arc — атомарным для потоковB)Rc живёт на стеке, Arc — в кучеC)Rc освобождает память сразу, Arc откладывает до сборки мусораD)Rc для неизменяемых данных, Arc разрешает менять значение внутри
показать ответ и разбор
+A)Rc считает ссылки обычным счётчиком, Arc — атомарным для потоков// разбор: Функционально это один и тот же подсчёт ссылок, разница в цене синхронизации. У Rc счётчик неатомарный и потому дешёвый, но тип не Send — компилятор не пустит его в другой поток. Arc платит атомарными операциями и потому пригоден для многопотока. Мутабельности не даёт ни тот, ни другой: нужен RefCell или Mutex внутри.
- Зачем в однопоточном коде связка Rc<RefCell<T>>?A)Rc хранит данные, RefCell кэширует их копию для быстрого чтенияB)Rc даёт несколько владельцев, RefCell — правку через &C)Rc защищает от гонок, RefCell от паник при обращенииD)Такая связка нужна, чтобы значение пережило текущую функцию
показать ответ и разбор
+B)Rc даёт несколько владельцев, RefCell — правку через &// разбор: Rc позволяет нескольким местам владеть значением, но отдаёт только & — менять нельзя. RefCell добавляет внутреннюю изменяемость: правило XOR проверяется в рантайме, а не компилятором. Вместе получается разделяемое изменяемое состояние в одном потоке — типовая основа графов и деревьев с обратными ссылками.
- Два узла ссылаются друг на друга через Rc. Что произойдёт при выходе из области видимости?A)Сборщик циклов обнаружит их и освободит памятьB)Компилятор запретит такую конструкцию на этапе сборкиC)Память освободится: Rc отслеживает циклы через счётчик слабых ссылокD)Счётчики не дойдут до нуля, деструкторы не вызовутся — утечка
показать ответ и разбор
+D)Счётчики не дойдут до нуля, деструкторы не вызовутся — утечка// разбор: Взаимные Rc держат друг друга: после выхода из области видимости счётчики падают с двух до единицы и там застревают. Память не освобождается, Drop не вызывается — классическая утечка, и она безопасна с точки зрения языка. Лечится Weak: обратную ссылку делают слабой, она в счётчик владения не входит.
- Что вернёт Weak::upgrade(), если сильных ссылок на значение уже не осталось?A)Rc с новым счётчиком: upgrade воскрешает значениеB)Ссылку на освобождённую память — отсюда unsafe в сигнатуреC)None: данные освобождены, слабая ссылка знает об этомD)Панику: обращение к мёртвому значению считается ошибкой
показать ответ и разбор
+C)None: данные освобождены, слабая ссылка знает об этом// разбор: Weak не удерживает данные: при обнулении сильного счётчика значение уничтожается, а служебный блок с счётчиками живёт, пока есть слабые ссылки. Поэтому upgrade возвращает Option — Some, пока значение живо, и None после. Так делают кэши и обратные ссылки «ребёнок → родитель», не создавая циклов.
- Что напечатает код?
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, затем 1B)1, затем 2, затем 1C)1, затем 2, затем 2D)2, затем 3, затем 2показать ответ и разбор
+B)1, затем 2, затем 1// разбор: Rc::clone не копирует строку, а увеличивает счётчик сильных ссылок: внутри блока владельцев двое. На выходе из блока _b уничтожается, счётчик падает обратно до единицы, и значение живо, пока он не дошёл до нуля. Отсюда и цена приёма: сам клон дешёвый, но каждый требует парного drop, а взаимные ссылки счётчик до нуля не доводят вовсе.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.