Rust - Basic - 14 - Умные указатели

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

Кратко

Типы, реализующие трейты Deref и Drop, можно использовать как умные указатели

Box

Три случая использования Box

  • когда размер типа неизвестен во время выполнения
  • когда данных слишком много и нужно избежать накладных расходов на copy при передаче ownership
  • когда нужно own a value, но конкретный тип не важен — важен лишь определённый trait

На рисунке ниже — упрощённая схема вывода памяти компилятором Rust для recursive type; в итоге код не компилируется

// следующий код не скомпилируется
enum List {
    Cons(i32, List),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Cons(2, Cons(3, Nil)));
}

1

Здесь можно применить Box, и компилятор сможет вывести размер памяти для соответствующего типа

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}

2

Deref & Drop

Когда переменная типа передаётся как параметр функции и тип параметра с этим типом не совпадает, компилятор Rust попытается выполнить принудительное разыменование (то есть вызвать method deref трейта Deref)

Приведение через Deref (deref coercion) происходит автоматически, когда мы передаём ссылку на значение некоторого типа как аргумент функции или методу, у которых тип параметра не совпадает с переданным.

method drop трейта Drop вызывается, когда value goes out of scope (FILO)

method drop нельзя вызывать вручную

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}
// сначала вызывается drop для d, затем drop для c
fn main() {
    let c = CustomSmartPointer {
        data: String::from("my stuff"),
    };
    let d = CustomSmartPointer {
        data: String::from("other stuff"),
    };
    println!("CustomSmartPointers created.");
}

// drop нельзя вызывать вручную, следующий код не скомпилируется
fn main() {
    let c = CustomSmartPointer {
        data: String::from("some data"),
    };
    println!("CustomSmartPointer created.");
    c.drop();
    println!("CustomSmartPointer dropped before the end of main.");
}
// можно косвенно вызвать drop через std::mem:drop
fn main() {
    let c = CustomSmartPointer {
        data: String::from("some data"),
    };
    println!("CustomSmartPointer created.");
    drop(c);
    println!("CustomSmartPointer dropped before the end of main.");
}

Rc

подсчёт ссылок; можно применять для реализации графа

// базовый синтаксис
enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));  // clone даёт поверхностное копирование
    let c = Cons(4, Rc::clone(&a));
}

// strong_count показывает, сколько сейчас ссылок на переменную
fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

RefCell

В отличие от Box и Rc, RefCell позволяет проверять immutable or mutable borrows во время выполнения (runtime)

если проверка не проходит, программа паникует (panic)

Как выбирать между Box<T>, Rc<T> и RefCell<T>:

  • Rc<T> допускает несколько owner, Box<T> и RefCell<T> — только одного

    Rc<T> позволяет нескольким владельцам разделять одни и те же данные; Box<T> и RefCell<T> имеют одного владельца.

  • Box<T> допускает проверку immutable or mutable borrows на этапе компиляции; Rc<T> — только immutable borrows на этапе компиляции; RefCell<T> — immutable or mutable borrows во время выполнения

    Box<T> позволяет проверять immutable или mutable borrows на этапе компиляции; Rc<T> — только immutable borrows на этапе компиляции; RefCell<T> — immutable или mutable borrows во время выполнения.

  • поскольку RefCell<T> допускает проверку mutable borrows во время выполнения, можно менять значение внутри RefCell<T>, даже если сам RefCell<T> неизменяемый

    Поскольку RefCell<T> позволяет проверять mutable borrows во время выполнения, можно изменять значение внутри RefCell<T>, даже когда сам RefCell<T> неизменяемый.

// базовый синтаксис — в паре с Rc
#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let value = Rc::new(RefCell::new(5));

    let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

    let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));

    *value.borrow_mut() += 10;

    println!("a after = {:?}", a);
    println!("b after = {:?}", b);
    println!("c after = {:?}", c);
}

Weak

Использование сильных ссылок Strong<T> может приводить к циклическим ссылкам и, как следствие, к утечке памяти

С помощью Rc::downgrade можно создать слабую ссылку Weak<T>, при этом weak_count увеличивается на 1

Когда strong_count равен 0, соответствующая ref освобождается, даже если weak_count не равен 0

Для слабой ссылки нужно вызывать method upgrade, чтобы динамически получить данные, на которые она ссылается (возвращаемый тип — Option)

// на этом примере хорошо видна разница между слабой и сильной ссылкой
use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    );

    {
        let branch = Rc::new(Node {
            value: 5,
            parent: RefCell::new(Weak::new()),
            children: RefCell::new(vec![Rc::clone(&leaf)]),
        });

        *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

        println!(
            "branch strong = {}, weak = {}",
            Rc::strong_count(&branch),
            Rc::weak_count(&branch),
        );

        println!(
            "leaf strong = {}, weak = {}",
            Rc::strong_count(&leaf),
            Rc::weak_count(&leaf),
        );
    }

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
    println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    );
}

Приведённая программа выводит следующее

leaf strong = 1, weak = 0
branch strong = 1, weak = 1
leaf strong = 2, weak = 0
leaf parent = None
leaf strong = 1, weak = 0

Источник

https://doc.rust-lang.org/book/ch15-00-smart-pointers.html

…

Использование Box<T> для указания на данные в куче

Самый прямолинейный умный указатель — box, его тип записывается как Box<T>. Box позволяет хранить данные в куче, а не в стеке; в стеке остаётся указатель на данные в куче. Обратитесь к главе 4, чтобы повторить разницу между стеком и кучей.

У box нет накладных расходов на производительность, кроме самого хранения данных в куче вместо стека. Но и дополнительных возможностей у него немного. Чаще всего вы будете использовать их в таких ситуациях:

  • когда у вас есть тип, размер которого неизвестен на этапе компиляции, и вы хотите использовать значение этого типа там, где требуется точный размер
  • когда у вас много данных и вы хотите передать ownership, но гарантировать, что при этом данные не будут скопированы
  • когда вы хотите владеть значением, и вам важно только, что это тип, реализующий определённый трейт, а не какой-то конкретный тип

Первую ситуацию мы покажем в разделе «Включение рекурсивных типов с помощью Box» . Во втором случае передача ownership большого объёма данных может занимать много времени, потому что данные копируются по стеку. Чтобы повысить производительность в этой ситуации, можно хранить большой объём данных в куче в box; тогда по стеку копируются только небольшие данные указателя, а данные, на которые он ссылается, остаются в одном месте в куче. Третий случай известен как trait object: глава 17 посвящает ему целый раздел «Использование trait objects для значений разных типов» . Так что то, что вы узнаете здесь, снова пригодится в главе 17!