Добро пожаловать в главу 20 нашего курса по Rust, где мы погружаемся в удивительный мир умных указателей. Если вы дошли до этого момента, вы уже знакомы с основами владения, заимствования и базовыми типами данных в Rust. Теперь пришло время расширить ваш арсенал инструментов для управления памятью с помощью умных указателей — мощных абстракций, которые позволяют обойти строгие правила владения и заимствования там, где это необходимо, сохраняя при этом безопасность памяти. Эта лекция будет максимально подробной, самодостаточной и ориентированной как на новичков, так и на тех, кто стремится к экспертному уровню. Мы разберём каждый аспект темы, предоставим примеры кода с комментариями, обсудим нюансы и подводные камни, а в конце реализуем практическое упражнение.
Rust славится своей строгой системой владения, которая предотвращает ошибки вроде двойного освобождения памяти или использования указателей на освобождённые данные. Однако иногда правила "один владелец" и "неизменяемость по умолчанию" становятся слишком ограничивающими. Например, что делать, если вам нужно:
Для таких случаев в стандартной библиотеке Rust существуют умные указатели — структуры, которые оборачивают данные и предоставляют дополнительные возможности управления памятью. Они реализуют трейты Deref
и Drop
, что позволяет им вести себя как обычные указатели, но с добавленной логикой. В этой главе мы разберём основные умные указатели: Box
, Rc
, Arc
, RefCell
и Cell
.
Box<T>
— это простейший умный указатель, который выделяет память для значения типа T
в куче и владеет им. Когда Box
выходит из области видимости, память автоматически освобождается благодаря реализации трейта Drop
.
Box
, чтобы не перегружать стек.fn main() {
let boxed_int = Box::new(42); // Выделяем 42 в куче
println!("Значение в Box: {}", *boxed_int); // Разыменование через *
}
Box
реализует Deref
, поэтому вы можете использовать *boxed_int
или вызывать методы напрямую (например, boxed_int.to_string()
).Box
фиксирован (обычно размер указателя, 8 байт на 64-битных системах), независимо от размера T
.Box
не решает проблему множественного владения — у него всегда один владелец.Box
после перемещения владения, компилятор вас остановит.Обычный односвязный список нельзя реализовать без Box
, так как его размер зависит от длины, а Rust требует фиксированный размер на этапе компиляции:
enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
}
Rc<T>
(Reference Counted): Умный указатель для подсчёта ссылок, позволяющий иметь несколько владельцев одного значения в куче. Используется в однопоточных приложениях.Arc<T>
(Atomic Reference Counted): То же самое, но безопасно для многопоточных приложений благодаря атомарным операциям.Rc
или Arc
создаётся счётчик ссылок..clone()
увеличивает счётчик.use std::rc::Rc;
fn main() {
let data = Rc::new(42); // Создаём значение в куче
let data_clone1 = data.clone(); // Увеличиваем счётчик (теперь 2)
let data_clone2 = data.clone(); // Счётчик = 3
println!("Значение: {}", *data); // 42
} // Здесь память освобождается, так как все ссылки исчезают
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(42);
let data_clone = Arc::clone(&data); // Атомарное клонирование
let handle = thread::spawn(move || {
println!("Значение в потоке: {}", *data_clone);
});
handle.join().unwrap();
println!("Значение в основном потоке: {}", *data);
}
Rc::clone(&rc)
— это не глубокое копирование, а лишь увеличение счётчика.Rc
не позволяет изменять данные внутри себя (для этого нужен RefCell
).Arc
дороже по производительности из-за атомарных операций.Rc
, указывающих друг на друга). Для этого есть Weak<T>
.Cell<T>
: Предоставляет изменяемость для типов, которые можно копировать (например, i32
), без проверки заимствования на этапе компиляции.RefCell<T>
: Позволяет изменять данные через неизменяемую ссылку, перенося проверку заимствования в runtime.Rust требует, чтобы изменяемость была явной (&mut
), но иногда это невозможно (например, в структурах с общим владением). Cell
и RefCell
решают эту проблему.
use std::cell::Cell;
fn main() {
let value = Cell::new(42);
println!("Старое значение: {}", value.get()); // 42
value.set(100); // Изменяем значение
println!("Новое значение: {}", value.get()); // 100
}
use std::cell::RefCell;
fn main() {
let value = RefCell::new(42);
let mut_ref = value.borrow_mut(); // Получаем изменяемую ссылку
*mut_ref = 100;
drop(mut_ref); // Освобождаем заимствование
println!("Значение: {}", value.borrow()); // 100
}
Cell
работает только с Copy
-типами, для остальных нужен RefCell
.RefCell
паникует при нарушении правил заимствования в runtime (например, если есть активная borrow_mut()
и вы вызываете borrow()
).Rc<RefCell<T>>
— популярный паттерн для общего владения с изменяемостью.RefCell
в цикле или рекурсии может привести к панике из-за неожиданных заимствований.Cell
не даёт ссылок, только копии или замену значения.Box
, Rc
, etc.) и управляется указателями.Box
, Rc
, Arc
выделяют данные в куче, но сам указатель живёт в стеке.fn main() {
let stack_value = 42; // В стеке
let heap_value = Box::new(42); // В куче
println!("Стек: {}, Куча: {}", stack_value, *heap_value);
}
Указатель | Владение | Изменяемость | Потоки | Использование |
---|---|---|---|---|
Box<T> |
Один владелец | Да (через &mut ) |
Нет | Рекурсия, большие данные |
Rc<T> |
Много владельцев | Нет | Нет | Общее владение в однопоточности |
Arc<T> |
Много владельцев | Нет | Да | Общее владение в многопоточности |
Cell<T> |
Один владелец | Да (копии) | Нет | Простая внутренняя изменяемость |
RefCell<T> |
Один владелец | Да (runtime) | Нет | Сложная внутренняя изменяемость |
Box
.Rc
.Arc
.&mut
? → Cell
(простые типы) или RefCell
(сложные).Давайте реализуем простое дерево, где узлы могут иметь несколько родителей (граф):
use std::rc::Rc;
use std::cell::RefCell;
struct Node {
value: i32,
children: Vec<Rc<RefCell<Node>>>,
}
fn main() {
let root = Rc::new(RefCell::new(Node {
value: 1,
children: Vec::new(),
}));
let child = Rc::new(RefCell::new(Node {
value: 2,
children: Vec::new(),
}));
root.borrow_mut().children.push(child.clone());
println!("Root value: {}", root.borrow().value); // 1
println!("Child value: {}", child.borrow().value); // 2
}
Rc
позволяет узлам иметь несколько родителей.RefCell
даёт возможность изменять children
через неизменяемую ссылку.Реализуйте бинарное дерево, где каждый узел имеет значение и два дочерних узла (left
и right
). Добавьте метод для вставки значений и обхода дерева.
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct TreeNode {
value: i32,
left: Option<Rc<RefCell<TreeNode>>>,
right: Option<Rc<RefCell<TreeNode>>>,
}
impl TreeNode {
fn new(value: i32) -> Self {
TreeNode {
value,
left: None,
right: None,
}
}
fn insert(&mut self, value: i32) {
if value < self.value {
match &self.left {
Some(left) => left.borrow_mut().insert(value),
None => self.left = Some(Rc::new(RefCell::new(TreeNode::new(value)))),
}
} else {
match &self.right {
Some(right) => right.borrow_mut().insert(value),
None => self.right = Some(Rc::new(RefCell::new(TreeNode::new(value)))),
}
}
}
fn inorder(&self) {
if let Some(ref left) = self.left {
left.borrow().inorder();
}
println!("{}", self.value);
if let Some(ref right) = self.right {
right.borrow().inorder();
}
}
}
fn main() {
let root = Rc::new(RefCell::new(TreeNode::new(5)));
root.borrow_mut().insert(3);
root.borrow_mut().insert(7);
root.borrow_mut().insert(1);
root.borrow_mut().insert(9);
println!("Inorder traversal:");
root.borrow().inorder(); // 1, 3, 5, 7, 9
}
Rc
позволяет узлам быть разделяемыми.RefCell
даёт изменяемость внутри дерева.insert
добавляет значения по правилам бинарного дерева поиска.inorder
выполняет симметричный обход (левый-корень-правый).borrow()
и borrow_mut()
аккуратно, чтобы избежать паники.Умные указатели — это мощный инструмент в арсенале Rust-разработчика. Они позволяют гибко управлять памятью, обходя строгие правила владения и заимствования, но требуют понимания их ограничений и подводных камней. Практикуйтесь с Box
, Rc
, Arc
, Cell
и RefCell
, чтобы почувствовать, как они работают в реальных задачах. Удачи в освоении Rust!