Умные указатели в Rust
Правила владения и заимствования покрывают большинство ситуаций, но несколько реальных случаев в схему «ровно один владелец» не укладываются: значение, размер которого известен только во время работы программы; данные, которым действительно нужно несколько владельцев; значение, которое нужно менять через общую ссылку. Умные указатели (smart pointers) — ответ Rust на каждый из этих случаев.
Box<T>: кладём значение в кучу
Заголовок раздела «Box<T>: кладём значение в кучу»По умолчанию значения живут на стеке, и компилятору нужно заранее знать их размер. Box<T> переносит значение в кучу и даёт тебе указатель фиксированного размера на него. Это полезно, когда размер типа нельзя узнать во время компиляции, — чаще всего с рекурсивными типами:
fn main() { let boxed = Box::new(5); println!("{}", *boxed); // * достаёт значение}Вывод:
5Классический пример, где Box необходим: тип, который содержит сам себя, например узел связного списка:
enum List { Cons(i32, Box<List>), Nil,}
fn main() { let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
match list { List::Cons(value, _) => println!("Первое значение: {value}"), List::Nil => println!("Пусто"), }}Вывод:
Первое значение: 1Без Box для хранения Cons(i32, List) понадобилось бы бесконечно много места: List, внутри которого List, внутри которого List, и так без конца. А Box<List> — указатель фиксированного размера, поэтому компилятор может посчитать, сколько места нужно.
Rc<T>: несколько владельцев
Заголовок раздела «Rc<T>: несколько владельцев»Владение говорит, что у значения ровно один владелец. Чаще всего это именно то, что нужно, но иногда нескольким частям программы действительно нужно делить одни и те же данные. Rc<T> (reference counted — «с подсчётом ссылок») это позволяет: он отслеживает, сколько владельцев есть в данный момент:
use std::rc::Rc;
fn main() { let a = Rc::new(String::from("общие данные")); let b = Rc::clone(&a); // не глубокая копия, а просто ещё один владелец let c = Rc::clone(&a);
println!("{a} {b} {c}"); println!("Владельцев: {}", Rc::strong_count(&a));}Вывод:
общие данные общие данные общие данныеВладельцев: 3Rc::clone не копирует сами данные: он просто увеличивает счётчик и отдаёт ещё одну ссылку на то же значение. Данные освобождаются, только когда все владельцы вышли из области видимости.
RefCell<T>: меняем значение через общую ссылку
Заголовок раздела «RefCell<T>: меняем значение через общую ссылку»Обычно правила заимствования проверяются во время компиляции: либо много ссылок только для чтения, либо одна изменяемая, но никогда не одновременно. RefCell<T> переносит ту же проверку на время работы программы, и это позволяет менять значение даже через то, что выглядит как общая ссылка без mut:
use std::cell::RefCell;
fn main() { let value = RefCell::new(5);
*value.borrow_mut() += 1;
println!("{}", value.borrow());}Вывод:
6RefCell чаще всего используют вместе с Rc: Rc<RefCell<T>> даёт значение с несколькими владельцами, которое всё ещё можно менять, — одному Rc<T> это не под силу.
Когда они действительно нужны
Заголовок раздела «Когда они действительно нужны»Не начинай с умных указателей. Начинай с обычного владения и заимствования и бери эти инструменты, только когда компилятор прямо говорит, что иначе не выйдет: рекурсивный тип (Box), значение, которому действительно нужно несколько владельцев (Rc), или изменение через общую ссылку, которое правила заимствования не разрешают (RefCell).
Практика
Заголовок раздела «Практика»1. Складываем связный список
Заголовок раздела «1. Складываем связный список»Возьми перечисление List из раздела про Box. Построй список 1, 2, 3 и напиши функцию fn sum(list: &List) -> i32, которая складывает все значения. Подсказка: используй match по списку. Для Cons(value, rest) ответ — value плюс сумма rest. Для Nil — 0.
Показать решение
enum List { Cons(i32, Box<List>), Nil,}
fn sum(list: &List) -> i32 { match list { List::Cons(value, rest) => value + sum(rest), List::Nil => 0, }}
fn main() { let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil)))))); println!("Сумма: {}", sum(&list));}Вывод:
Сумма: 6Функция вызывает сама себя для остатка списка, пока не дойдёт до Nil. Заметь, что rest (это &Box<List>) можно передать туда, где ожидается &List: Rust сам заглядывает внутрь Box.
2. Общий список покупок
Заголовок раздела «2. Общий список покупок»Сделай один список покупок (Vec<String>), которым владеют и в который могут добавлять два человека — alice и bob. Каждый добавляет по одной покупке. Затем выведи список и число владельцев. Подсказка: оберни Vec в Rc<RefCell<...>>, дай каждому по Rc::clone и добавляй через .borrow_mut().push(...).
Показать решение
use std::cell::RefCell;use std::rc::Rc;
fn main() { let list = Rc::new(RefCell::new(Vec::new())); let alice = Rc::clone(&list); let bob = Rc::clone(&list);
alice.borrow_mut().push(String::from("молоко")); bob.borrow_mut().push(String::from("хлеб"));
println!("{:?}", list.borrow()); println!("Владельцев: {}", Rc::strong_count(&list));}Вывод:
["молоко", "хлеб"]Владельцев: 3Владельцев трое: list, alice и bob. Все трое указывают на один и тот же Vec, поэтому покупка, добавленная через alice, видна и через bob, и через list. Ни одной переменной не нужен mut, потому что изменения берёт на себя RefCell.
Box<T>хранит значение в куче через указатель фиксированного размера — это нужно для рекурсивных типов.Rc<T>с помощью подсчёта ссылок даёт одному значению несколько владельцев в однопоточном коде; между потоками используйArc<T>.RefCell<T>проверяет правила заимствования во время работы, а не при компиляции, и позволяет менять значение через общую ссылку, но паникует, если правила нарушены.- Бери эти инструменты, только когда обычных владения и заимствования не хватает.
Проверь себя
Заголовок раздела «Проверь себя»Проверь себя
1. Почему рекурсивному типу вроде узла связного списка нужен Box<T>?
2. Что на самом деле делает Rc::clone(&a)?
3. Когда RefCell<T> проверяет правила заимствования?
4. Что даёт Rc<RefCell<T>> по сравнению с одним Rc<T>?
Результат: 0 / 4
Что почитать
Заголовок раздела «Что почитать»- Использование
Box<T>для указания на данные в куче — книга «Язык программирования Rust» (перевод на русский): вся историяBox, включая пример cons-списка с этой страницы. Rc<T>, умный указатель с подсчётом ссылок — книга «Язык программирования Rust» (перевод на русский): общее владение шаг за шагом — видно, как счётчик растёт и уменьшается.RefCell<T>и внутренняя изменяемость — книга «Язык программирования Rust» (перевод на русский): когда и зачем переносить проверки заимствования на время работы, с реалистичным примером.std::cell— документация стандартной библиотеки (на английском): объясняетCellиRefCellи когда выбирать каждый из них.- Rc — Rust на примерах (перевод на русский): короткий запускаемый пример, который выводит счётчик ссылок, пока владельцы появляются и исчезают.