Лекция по главе 20: Умные указатели в Rust

Содержание: Введение в умные указатели Box: владение на куче Rc и Arc: подсчёт ссылок RefCell и Cell: внутренняя изменяемость Стек vs куча: как Rust управляет памятью Сравнение умных указателей и случаи применения Примеры: управление памятью в дереве Упражнение: реализация дерева с Rc и RefCell

Добро пожаловать в главу 20 нашего курса по Rust, где мы погружаемся в удивительный мир умных указателей. Если вы дошли до этого момента, вы уже знакомы с основами владения, заимствования и базовыми типами данных в Rust. Теперь пришло время расширить ваш арсенал инструментов для управления памятью с помощью умных указателей — мощных абстракций, которые позволяют обойти строгие правила владения и заимствования там, где это необходимо, сохраняя при этом безопасность памяти. Эта лекция будет максимально подробной, самодостаточной и ориентированной как на новичков, так и на тех, кто стремится к экспертному уровню. Мы разберём каждый аспект темы, предоставим примеры кода с комментариями, обсудим нюансы и подводные камни, а в конце реализуем практическое упражнение.


1. Введение в умные указатели

Rust славится своей строгой системой владения, которая предотвращает ошибки вроде двойного освобождения памяти или использования указателей на освобождённые данные. Однако иногда правила "один владелец" и "неизменяемость по умолчанию" становятся слишком ограничивающими. Например, что делать, если вам нужно:

Для таких случаев в стандартной библиотеке Rust существуют умные указатели — структуры, которые оборачивают данные и предоставляют дополнительные возможности управления памятью. Они реализуют трейты Deref и Drop, что позволяет им вести себя как обычные указатели, но с добавленной логикой. В этой главе мы разберём основные умные указатели: Box, Rc, Arc, RefCell и Cell.


2. Box: владение на куче

Что такое Box?

Box<T> — это простейший умный указатель, который выделяет память для значения типа T в куче и владеет им. Когда Box выходит из области видимости, память автоматически освобождается благодаря реализации трейта Drop.

Зачем нужен Box?

Пример: простой Box

fn main() {
    let boxed_int = Box::new(42); // Выделяем 42 в куче
    println!("Значение в Box: {}", *boxed_int); // Разыменование через *
}

Нюансы

Подводные камни

Пример: рекурсивный список

Обычный односвязный список нельзя реализовать без 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))));
}

3. Rc и Arc: подсчёт ссылок

Что такое Rc и Arc?

Как это работает?

Пример: Rc

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
} // Здесь память освобождается, так как все ссылки исчезают

Arc для многопоточности

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);
}

Нюансы

Подводные камни


4. RefCell и Cell: внутренняя изменяемость

Что такое RefCell и Cell?

Зачем нужны?

Rust требует, чтобы изменяемость была явной (&mut), но иногда это невозможно (например, в структурах с общим владением). Cell и RefCell решают эту проблему.

Пример: Cell

use std::cell::Cell;

fn main() {
    let value = Cell::new(42);
    println!("Старое значение: {}", value.get()); // 42
    value.set(100); // Изменяем значение
    println!("Новое значение: {}", value.get()); // 100
}

Пример: RefCell

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
}

Нюансы

Подводные камни


5. Стек vs куча: как Rust управляет памятью

Основы

Как Rust использует память?

Пример: стек против кучи

fn main() {
    let stack_value = 42; // В стеке
    let heap_value = Box::new(42); // В куче
    println!("Стек: {}, Куча: {}", stack_value, *heap_value);
}

Нюансы


6. Сравнение умных указателей и случаи применения

Указатель Владение Изменяемость Потоки Использование
Box<T> Один владелец Да (через &mut) Нет Рекурсия, большие данные
Rc<T> Много владельцев Нет Нет Общее владение в однопоточности
Arc<T> Много владельцев Нет Да Общее владение в многопоточности
Cell<T> Один владелец Да (копии) Нет Простая внутренняя изменяемость
RefCell<T> Один владелец Да (runtime) Нет Сложная внутренняя изменяемость

Когда что использовать?


7. Примеры: управление памятью в дереве

Давайте реализуем простое дерево, где узлы могут иметь несколько родителей (граф):

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
}

Объяснение


8. Упражнение: реализация дерева с Rc и RefCell

Задача

Реализуйте бинарное дерево, где каждый узел имеет значение и два дочерних узла (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
}

Разбор

Нюансы


Заключение

Умные указатели — это мощный инструмент в арсенале Rust-разработчика. Они позволяют гибко управлять памятью, обходя строгие правила владения и заимствования, но требуют понимания их ограничений и подводных камней. Практикуйтесь с Box, Rc, Arc, Cell и RefCell, чтобы почувствовать, как они работают в реальных задачах. Удачи в освоении Rust!