Перейти к содержимому

Умные указатели в Rust

Правила владения и заимствования покрывают большинство ситуаций, но несколько реальных случаев в схему «ровно один владелец» не укладываются: значение, размер которого известен только во время работы программы; данные, которым действительно нужно несколько владельцев; значение, которое нужно менять через общую ссылку. Умные указатели (smart pointers) — ответ Rust на каждый из этих случаев.

По умолчанию значения живут на стеке, и компилятору нужно заранее знать их размер. 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> (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));
}

Вывод:

общие данные общие данные общие данные
Владельцев: 3
▶ Попробуй сам

Rc::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());
}

Вывод:

6
▶ Попробуй сам

RefCell чаще всего используют вместе с Rc: Rc<RefCell<T>> даёт значение с несколькими владельцами, которое всё ещё можно менять, — одному Rc<T> это не под силу.

Не начинай с умных указателей. Начинай с обычного владения и заимствования и бери эти инструменты, только когда компилятор прямо говорит, что иначе не выйдет: рекурсивный тип (Box), значение, которому действительно нужно несколько владельцев (Rc), или изменение через общую ссылку, которое правила заимствования не разрешают (RefCell).

Возьми перечисление 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.

Сделай один список покупок (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