Box<T> в Rust
Что вы узнаете: умные указатели Rust —
Box<T>для выделения памяти в куче,Rc<T>для совместного владения иCell<T>/RefCell<T>для внутренней изменяемости. Они опираются на понятия владения и времён жизни из предыдущих разделов. Вы также кратко познакомитесь сWeak<T>для разрыва циклов ссылок.
Зачем Box<T>? В C для выделения памяти в куче используют malloc/free. В C++ std::unique_ptr<T> оборачивает new/delete. Box<T> в Rust — это аналог: указатель в куче с единственным владельцем, который автоматически освобождается при выходе из области видимости. В отличие от malloc, нет парного free, который можно забыть. В отличие от unique_ptr, нет использования после перемещения — компилятор полностью это предотвращает.
Когда использовать Box, а когда выделение на стеке:
-
Содержащийся тип большой, и вы не хотите копировать его на стеке
-
Нужен рекурсивный тип (например, узел связного списка, который содержит сам себя)
-
Нужны трейт-объекты (
Box<dyn Trait>) -
Box<T>можно использовать, чтобы создать указатель на тип, размещённый в куче. Размер указателя всегда фиксирован, независимо от типа<T>
fn main() {
// Создаём указатель на целое число (со значением 42), размещённое в куче
let f = Box::new(42);
println!("{} {}", *f, f);
// Клонирование box создаёт новое выделение в куче
let mut g = f.clone();
*g = 43;
println!("{f} {g}");
// g и f выходят из области видимости здесь и автоматически освобождаются
}
graph LR
subgraph "Стек"
F["f: Box<i32>"]
G["g: Box<i32>"]
end
subgraph "Куча"
HF["42"]
HG["43"]
end
F -->|"владеет"| HF
G -->|"владеет (клон)"| HG
style F fill:#51cf66,color:#000,stroke:#333
style G fill:#51cf66,color:#000,stroke:#333
style HF fill:#91e5a3,color:#000,stroke:#333
style HG fill:#91e5a3,color:#000,stroke:#333
Визуализация владения и заимствования
C/C++ и Rust: управление указателями и владением
// C — ручное управление памятью, потенциальные проблемы
void c_pointer_problems() {
int* ptr1 = malloc(sizeof(int));
*ptr1 = 42;
int* ptr2 = ptr1; // Оба указывают на одну и ту же память
int* ptr3 = ptr1; // Три указателя на одну и ту же память
free(ptr1); // Освобождает память
*ptr2 = 43; // Использование после освобождения — неопределённое поведение!
*ptr3 = 44; // Использование после освобождения — неопределённое поведение!
}
Для программистов C++: умные указатели помогают, но не предотвращают все проблемы:
// C++ — умные указатели помогают, но не предотвращают все проблемы void cpp_pointer_issues() { auto ptr1 = std::make_unique<int>(42); // auto ptr2 = ptr1; // Ошибка компиляции: unique_ptr нельзя копировать auto ptr2 = std::move(ptr1); // Ок: владение передано // Но C++ всё равно допускает использование после перемещения: // std::cout << *ptr1; // Компилируется! Но это неопределённое поведение! // Псевдонимы shared_ptr: auto shared1 = std::make_shared<int>(42); auto shared2 = shared1; // Оба владеют данными // Кто «на самом деле» владеет? Никто. Накладные расходы на счётчик ссылок везде. }
#![allow(unused)]
fn main() {
// Rust — система владения предотвращает эти проблемы
fn rust_ownership_safety() {
let data = Box::new(42); // data владеет выделением в куче
let moved_data = data; // Владение передано moved_data
// data больше недоступна — ошибка компиляции при использовании
let borrowed = &moved_data; // Неизменяемое заимствование
println!("{}", borrowed); // Безопасно использовать
// moved_data автоматически освобождается при выходе из области видимости
}
}
graph TD
subgraph "Проблемы управления памятью в C/C++"
CP1["int* ptr1"] --> CM["Память в куче<br/>значение: 42"]
CP2["int* ptr2"] --> CM
CP3["int* ptr3"] --> CM
CF["free(ptr1)"] --> CM_F["[ОШИБКА] Освобождённая память"]
CP2 -.->|"Использование после освобождения<br/>Неопределённое поведение"| CM_F
CP3 -.->|"Использование после освобождения<br/>Неопределённое поведение"| CM_F
end
subgraph "Система владения Rust"
RO1["data: Box<i32>"] --> RM["Память в куче<br/>значение: 42"]
RO1 -.->|"Передача владения"| RO2["moved_data: Box<i32>"]
RO2 --> RM
RO1_X["data: [ВНИМАНИЕ] ПЕРЕМЕЩЕНО<br/>Доступа нет"]
RB["&moved_data<br/>Неизменяемое заимствование"] -.->|"Безопасная ссылка"| RM
RD["Drop автоматически<br/>при выходе из области видимости"] --> RM
end
style CM_F fill:#ff6b6b,color:#000
style CP2 fill:#ff6b6b,color:#000
style CP3 fill:#ff6b6b,color:#000
style RO1_X fill:#ffa07a,color:#000
style RO2 fill:#51cf66,color:#000
style RB fill:#91e5a3,color:#000
style RD fill:#91e5a3,color:#000
Визуализация правил заимствования
#![allow(unused)]
fn main() {
fn borrowing_rules_example() {
let mut data = vec![1, 2, 3, 4, 5];
// Несколько неизменяемых заимствований — Ок
let ref1 = &data;
let ref2 = &data;
println!("{:?} {:?}", ref1, ref2); // Оба можно использовать
// Изменяемое заимствование — эксклюзивный доступ
let ref_mut = &mut data;
ref_mut.push(6);
// ref1 и ref2 нельзя использовать, пока активна ref_mut
// Когда ref_mut закончена, неизменяемые заимствования снова работают
let ref3 = &data;
println!("{:?}", ref3);
}
}
graph TD
subgraph "Правила заимствования в Rust"
D["mut data: Vec<i32>"]
subgraph "Фаза 1: несколько неизменяемых заимствований [OK]"
IR1["&data (ref1)"]
IR2["&data (ref2)"]
D --> IR1
D --> IR2
IR1 -.->|"Доступ только для чтения"| MEM1["Память: [1,2,3,4,5]"]
IR2 -.->|"Доступ только для чтения"| MEM1
end
subgraph "Фаза 2: эксклюзивное изменяемое заимствование [OK]"
MR["&mut data (ref_mut)"]
D --> MR
MR -.->|"Эксклюзивное чтение/запись"| MEM2["Память: [1,2,3,4,5,6]"]
BLOCK["[ОШИБКА] Остальные заимствования заблокированы"]
end
subgraph "Фаза 3: снова неизменяемые заимствования [OK]"
IR3["&data (ref3)"]
D --> IR3
IR3 -.->|"Доступ только для чтения"| MEM3["Память: [1,2,3,4,5,6]"]
end
end
subgraph "Что разрешают C/C++ (опасно)"
CP["int* ptr"]
CP2["int* ptr2"]
CP3["int* ptr3"]
CP --> CMEM["Та же память"]
CP2 --> CMEM
CP3 --> CMEM
RACE["[ОШИБКА] Возможны гонки данных<br/>[ОШИБКА] Возможно использование после освобождения"]
end
style MEM1 fill:#91e5a3,color:#000
style MEM2 fill:#91e5a3,color:#000
style MEM3 fill:#91e5a3,color:#000
style BLOCK fill:#ffa07a,color:#000
style RACE fill:#ff6b6b,color:#000
style CMEM fill:#ff6b6b,color:#000
Внутренняя изменяемость: Cell<T> и RefCell<T>
Напомним, что по умолчанию переменные в Rust неизменяемые. Иногда хочется, чтобы большая часть типа была доступна только для чтения, но при этом разрешалась запись в одно поле.
#![allow(unused)]
fn main() {
struct Employee {
employee_id : u64, // Это поле должно быть неизменяемым
on_vacation: bool, // А что, если мы хотим разрешить запись в это поле, но сделать employee_id неизменяемым?
}
}
- Напомним, что Rust допускает одну изменяемую ссылку на переменную и любое количество неизменяемых ссылок — это проверяется на этапе компиляции
- Что, если мы хотим передать неизменяемый вектор сотрудников, но при этом разрешить обновлять поле
on_vacation, гарантируя, чтоemployee_idнельзя будет изменить?
Cell<T> — внутренняя изменяемость для типов Copy
Cell<T>обеспечивает внутреннюю изменяемость, то есть доступ на запись к отдельным элементам через ссылки, которые в остальном доступны только для чтения- Работает копированием значений внутрь и наружу (для
.get()требуетсяT: Copy)
RefCell<T> — внутренняя изменяемость с проверкой заимствований во время выполнения
RefCell<T>— вариант, который работает со ссылками- Применяет проверку заимствований Rust во время выполнения, а не на этапе компиляции
- Допускает одно изменяемое заимствование, но вызывает panic, если есть любые другие активные ссылки
- Используйте
.borrow()для неизменяемого доступа и.borrow_mut()для изменяемого
Когда выбрать Cell, а когда RefCell
| Критерий | Cell<T> | RefCell<T> |
|---|---|---|
| Работает с | Типами Copy (целые, bool, числа с плавающей точкой) | Любыми типами (String, Vec, структуры) |
| Схема доступа | Копирует значения внутрь/наружу (.get(), .set()) | Заимствует на месте (.borrow(), .borrow_mut()) |
| Режим сбоя | Не может упасть — нет проверок во время выполнения | Panic, если берёте изменяемое заимствование, пока активно другое |
| Накладные расходы | Нулевые — просто копирование байтов | Небольшие — отслеживание состояния заимствования во время выполнения |
| Использовать, когда | Нужен изменяемый флаг, счётчик или небольшое значение внутри неизменяемой структуры | Нужно изменить String, Vec или сложный тип внутри неизменяемой структуры |
Совместное владение: Rc<T>
Rc<T> позволяет совместное владение неизменяемыми данными с подсчётом ссылок. Что, если мы хотим хранить одного и того же Employee в нескольких местах без копирования?
#[derive(Debug)]
struct Employee {
employee_id: u64,
}
fn main() {
let mut us_employees = vec![];
let mut all_global_employees = Vec::<Employee>::new();
let employee = Employee { employee_id: 42 };
us_employees.push(employee);
// Не скомпилируется — employee уже перемещён
//all_global_employees.push(employee);
}
Rc<T> решает эту проблему, разрешая совместный неизменяемый доступ:
- Содержащийся тип автоматически разыменовывается
- Тип уничтожается, когда счётчик ссылок становится 0
use std::rc::Rc;
#[derive(Debug)]
struct Employee {employee_id: u64}
fn main() {
let mut us_employees = vec![];
let mut all_global_employees = vec![];
let employee = Employee { employee_id: 42 };
let employee_rc = Rc::new(employee);
us_employees.push(employee_rc.clone());
all_global_employees.push(employee_rc.clone());
let employee_one = all_global_employees.get(0); // Совместная неизменяемая ссылка
for e in us_employees {
println!("{}", e.employee_id); // Совместная неизменяемая ссылка
}
println!("{employee_one:?}");
}
Для программистов C++: соответствие умных указателей
Умный указатель C++ Эквивалент в Rust Ключевое отличие std::unique_ptr<T>Box<T>Версия Rust используется по умолчанию — перемещение задаётся на уровне языка, а не включается явно std::shared_ptr<T>Rc<T>(один поток) /Arc<T>(несколько потоков)У Rcнет накладных расходов на атомарные операции;Arcиспользуйте, только когда делитесь данными между потокамиstd::weak_ptr<T>Weak<T>(изRc::downgrade()илиArc::downgrade())Та же цель: разрыв циклов ссылок Ключевое различие: в C++ вы выбираете использовать умные указатели. В Rust владеющие значения (
T) и заимствование (&T) покрывают большинство случаев — прибегайте кBox/Rc/Arc, только когда нужно выделение в куче или совместное владение.
Разрыв циклов ссылок с помощью Weak<T>
Rc<T> использует подсчёт ссылок — если два значения Rc указывают друг на друга, ни одно из них никогда не будет уничтожено (цикл). Weak<T> решает эту проблему:
use std::rc::{Rc, Weak};
struct Node {
value: i32,
parent: Option<Weak<Node>>, // Слабая ссылка — не мешает уничтожению
}
fn main() {
let parent = Rc::new(Node { value: 1, parent: None });
let child = Rc::new(Node {
value: 2,
parent: Some(Rc::downgrade(&parent)), // Слабая ссылка на родителя
});
// Чтобы использовать Weak, попробуйте повысить его — возвращается Option<Rc<T>>
if let Some(parent_rc) = child.parent.as_ref().unwrap().upgrade() {
println!("Parent value: {}", parent_rc.value);
}
println!("Parent strong count: {}", Rc::strong_count(&parent)); // 1, а не 2
}
Weak<T>подробнее рассматривается в главе Избегание избыточного clone(). Пока главное: используйтеWeakдля «обратных ссылок» в древовидных и графовых структурах, чтобы избежать утечек памяти.
Комбинирование Rc с внутренней изменяемостью
Настоящая мощь проявляется, когда вы комбинируете Rc<T> (совместное владение) с Cell<T> или RefCell<T> (внутренняя изменяемость). Это позволяет нескольким владельцам читать и изменять общие данные:
| Шаблон | Сценарий использования |
|---|---|
Rc<RefCell<T>> | Общие изменяемые данные (один поток) |
Arc<Mutex<T>> | Общие изменяемые данные (несколько потоков — см. ch13) |
Rc<Cell<T>> | Общие изменяемые типы Copy (простые флаги, счётчики) |
Упражнение: совместное владение и внутренняя изменяемость
🟡 Средний уровень
- Часть 1 (Rc): создайте структуру
Employeeс полямиemployee_id: u64иname: String. Поместите её вRc<Employee>и клонируйте в два отдельныхVec(us_employeesиglobal_employees). Выведите данные из обоих векторов, чтобы показать, что они разделяют одни и те же данные. - Часть 2 (Cell): добавьте поле
on_vacation: Cell<bool>вEmployee. Передайте неизменяемую ссылку&Employeeв функцию и переключитеon_vacationизнутри этой функции — без того, чтобы делать ссылку изменяемой. - Часть 3 (RefCell): замените
name: Stringнаname: RefCell<String>и напишите функцию, которая добавляет суффикс к имени сотрудника через&Employee(неизменяемую ссылку).
Стартовый код:
use std::cell::{Cell, RefCell};
use std::rc::Rc;
#[derive(Debug)]
struct Employee {
employee_id: u64,
name: RefCell<String>,
on_vacation: Cell<bool>,
}
fn toggle_vacation(emp: &Employee) {
// TODO: Переключите on_vacation с помощью Cell::set()
}
fn append_title(emp: &Employee, title: &str) {
// TODO: Заимствуйте name изменяемо через RefCell и добавьте title с помощью push_str
}
fn main() {
// TODO: Создайте сотрудника, оберните в Rc, клонируйте в два Vec,
// вызовите toggle_vacation и append_title, выведите результаты
}
Решение (нажмите, чтобы развернуть)
use std::cell::{Cell, RefCell};
use std::rc::Rc;
#[derive(Debug)]
struct Employee {
employee_id: u64,
name: RefCell<String>,
on_vacation: Cell<bool>,
}
fn toggle_vacation(emp: &Employee) {
emp.on_vacation.set(!emp.on_vacation.get());
}
fn append_title(emp: &Employee, title: &str) {
emp.name.borrow_mut().push_str(title);
}
fn main() {
let emp = Rc::new(Employee {
employee_id: 42,
name: RefCell::new("Alice".to_string()),
on_vacation: Cell::new(false),
});
let mut us_employees = vec![];
let mut global_employees = vec![];
us_employees.push(Rc::clone(&emp));
global_employees.push(Rc::clone(&emp));
// Переключаем отпуск через неизменяемую ссылку
toggle_vacation(&emp);
println!("On vacation: {}", emp.on_vacation.get()); // true
// Добавляем звание через неизменяемую ссылку
append_title(&emp, ", Sr. Engineer");
println!("Name: {}", emp.name.borrow()); // "Alice, Sr. Engineer"
// Оба Vec видят одни и те же данные (Rc разделяет владение)
println!("US: {:?}", us_employees[0].name.borrow());
println!("Global: {:?}", global_employees[0].name.borrow());
println!("Rc strong count: {}", Rc::strong_count(&emp));
}
// Вывод:
// On vacation: true
// Name: Alice, Sr. Engineer
// US: "Alice, Sr. Engineer"
// Global: "Alice, Sr. Engineer"
// Rc strong count: 3