Руководство Rust по попаданию в яму: умные указатели

Rust
Руководство Rust по попаданию в яму: умные указатели

После понимания концепций владения, заимствования владения и жизненного цикла в Rust, я считаю, что у вас есть глубокое понимание Rust.Сегодня еще одна серийная яма.Давайте спланируем умный указатель и исследуем его.в конце концов.

Умные указатели — это особый вид структуры данных в Rust. Существенная разница между ним и обычным указателем заключается в том, что обычный указатель является заимствованием значения, тогда как интеллектуальный указатель обычно владеет данными. В Rust, если вы хотите определить объект в куче памяти, вы не создаете новый объект напрямую, как в Java, и вам не нужно вручную использовать функцию malloc для выделения пространства памяти, как в языке C. Руст используетBox::newдля инкапсуляции данных иBox<T>Это один из умных указателей, которые мы собираемся представить сегодня. КромеBox<T>В дополнение к умным указателям, представленным в стандартной библиотеке Rust, естьRc<T>,Ref<T>,RefCell<T>и т.п. Прежде чем вдаваться в подробности, давайте сначала разберемся с основными понятиями интеллектуальных указателей.

Основные понятия

Мы сказали, что умный указатель Rust — это особая структура данных, так что же в нем особенного? Он отличается от обычных структур данных тем, что интеллектуальные указатели реализуютDerefа такжеDropЭти две черты. выполнитьDerefМожно включить разыменование интеллектуальных указателей при реализацииDropЗатем умный указатель имеет возможность автоматически уничтожать.

Deref

В Deref есть функция, обеспечивающая неявные преобразования:Если тип T реализует Deref, ссылка этого типа T будет автоматически преобразована в тип U при применении..

use std::rc::Rc;
fn main() {
    let x = Rc::new("hello");
    println!("{:?}", x.chars());
}

Если вы посмотрите на исходный код Rc, вы обнаружите, что он не реализует метод chars(), но приведенный выше код можно вызывать напрямую, потому что Rc реализует Deref.

#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> Deref for Rc<T> {
    type Target = T;

    #[inline(always)]
    fn deref(&self) -> &T {
        &self.inner().value
    }
}

Это приводит к тому, что интеллектуальный указатель автоматически разыменовывается при его использовании, как если бы он не существовал.

Внутренняя реализация Deref выглядит так:

#[lang = "deref"]
#[doc(alias = "*")]
#[doc(alias = "&*")]
#[stable(feature = "rust1", since = "1.0.0")]
pub trait Deref {
    /// The resulting type after dereferencing.
    #[stable(feature = "rust1", since = "1.0.0")]
    type Target: ?Sized;

    /// Dereferences the value.
    #[must_use]
    #[stable(feature = "rust1", since = "1.0.0")]
    fn deref(&self) -> &Self::Target;
}

#[lang = "deref_mut"]
#[doc(alias = "*")]
#[stable(feature = "rust1", since = "1.0.0")]
pub trait DerefMut: Deref {
    /// Mutably dereferences the value.
    #[stable(feature = "rust1", since = "1.0.0")]
    fn deref_mut(&mut self) -> &mut Self::Target;
}

DerefMut похож на Deref, за исключением того, что он возвращает изменяемую ссылку.

Drop

Удаление очень важно для интеллектуальных указателей. Оно автоматически выполняет некоторую работу по очистке, когда интеллектуальные указатели отбрасываются. Упомянутая здесь работа по очистке не ограничивается освобождением динамической памяти, но также включает некоторые действия, такие как освобождение файлов и сетевых подключений. Раньше я всегда понимал Drop как сборщик мусора в Java, но с глубоким пониманием этого я обнаружил, что он намного мощнее, чем сборщик мусора.

Внутренняя реализация Drop выглядит так:

#[lang = "drop"]
#[stable(feature = "rust1", since = "1.0.0")]
pub trait Drop {
    #[stable(feature = "rust1", since = "1.0.0")]
    fn drop(&mut self);
}

Здесь есть только один метод drop, и структура, реализующая Drop, вызовет метод drop до того, как он умрет.

use std::ops::Drop;
#[derive(Debug)]
struct S(i32);

impl Drop for S {
    fn drop(&mut self) {
        println!("drop {}", self.0);
    }
}

fn main() {
    let x = S(1);
    println!("create x: {:?}", x);
    {
        let y = S(2);
        println!("create y: {:?}", y);
    }
}

Результатом выполнения приведенного выше кода является

结果

Вы можете видеть, что и x, и y выполняют метод drop в конце жизненного цикла.

Здесь сначала представлены основные концепции интеллектуальных указателей, а теперь давайте перейдем к теме и рассмотрим характеристики каждого интеллектуального указателя.

Box

Ранее мы упоминали, что Box используется в Rust для хранения данных в динамической памяти. Его использование очень простое:

fn main() {
    let x = Box::new("hello");
    println!("{:?}", x.chars())
}

мы можем взглянутьBox::newисходный код

#[stable(feature = "rust1", since = "1.0.0")]
#[inline(always)]
pub fn new(x: T) -> Box<T> {
  box x
}

Как видите, здесь есть только одно ключевое слово box, которое используется для выделения памяти в куче и может использоваться только внутри исходного кода Rust. Ключевое слово box вызывает внутренние методы Rust exchange_malloc и box_free для управления памятью.

#[cfg(not(test))]
#[lang = "exchange_malloc"]
#[inline]
unsafe fn exchange_malloc(size: usize, align: usize) -> *mut u8 {
    if size == 0 {
        align as *mut u8
    } else {
        let layout = Layout::from_size_align_unchecked(size, align);
        let ptr = alloc(layout);
        if !ptr.is_null() {
            ptr
        } else {
            handle_alloc_error(layout)
        }
    }
}

#[cfg_attr(not(test), lang = "box_free")]
#[inline]
pub(crate) unsafe fn box_free<T: ?Sized>(ptr: Unique<T>) {
    let ptr = ptr.as_ptr();
    let size = size_of_val(&*ptr);
    let align = min_align_of_val(&*ptr);
    // We do not allocate for Box<T> when T is ZST, so deallocation is also not necessary.
    if size != 0 {
        let layout = Layout::from_size_align_unchecked(size, align);
        dealloc(ptr as *mut u8, layout);
    }
}

Rc

В предыдущем исследовании мы знали, что значение в Rust может одновременно принадлежать только одной переменной, но иногда нам может понадобиться владение несколькими переменными.Например, в структуре графа два графа могут принадлежать одной и той же переменной. край имеет право собственности.

Для таких случаев Rust предоставляет нам интеллектуальные указатели Rc (подсчет ссылок), чтобы решить проблему совместного владения. Всякий раз, когда мы делимся владением через Rc, счетчик ссылок увеличивается на единицу. Значение будет уничтожено только тогда, когда счетчик ссылок достигнет 0.

Rc — это однопоточный указатель со счетчиком ссылок, а не потокобезопасный тип.

Давайте рассмотрим применение Rc на простом примере. (пример изthe book)

Если мы хотим создать «двуглавый» связанный список, как показано на рисунке ниже, и 3, и 4 указывают на 5. Давайте сначала попробуем реализацию Box.

双头链表

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

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

fn main() {
    let a = Cons(5,
                 Box::new(Cons(10,
                               Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}

Приведенный выше код сообщит об ошибке во время компиляции, потому что после привязки a к b его нельзя снова привязать к c.

Box无法共享所有权

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));
    let c = Cons(4, Rc::clone(&a));
    println!("count a {}", Rc::strong_count(&a));
}

На данный момент мы видим, что счетчик ссылок a равен 3, потому что здесь вычисляется счетчик ссылок узла 5, а сам a также является привязкой к 5. Этот тип ссылки, которая разделяет владение с помощью метода клонирования, называетсясильная цитата.

Rust также предоставляет нам еще один интеллектуальный указатель Weak, который можно рассматривать как еще одну версию Rc. Он содержит ссылки наслабая ссылка. Указатель, который он разделяет, не имеет владельца. Но он может помочь нам эффективно избежать циклических ссылок.

RefCell

В предыдущей статье мы говорили о изменчивости и неизменяемости переменных, в основном для переменных. Как упоминалось ранее, для структуры мы можем контролировать только возможность изменения всего ее экземпляра. Независимо от того, является ли конкретный член экземпляра изменяемым или нет, мы не можем это контролировать. Но в реальной разработке такие сценарии встречаются довольно часто. Например, у нас есть структура User:

struct User {
    id: i32,
    name: str,
    age: u8,
}

Обычно мы можем изменить только имя или возраст человека, но не идентификатор пользователя. Если мы установим экземпляр User в изменяемое состояние, нет гарантии, что другие не изменят идентификатор.

Чтобы справиться с этой ситуацией, Rust предоставляет намCell<T>а такжеRefCell<T>. По сути, это не интеллектуальные указатели, а контейнеры, которые могут обеспечивать внутреннюю изменчивость. Внутренняя изменчивость на самом деле является шаблоном проектирования, внутреннее содержание которогоunsafeкод для достижения.

Давайте сначала посмотримCell<T>как это использовать.

use std::cell::Cell;
struct Foo {
    x: u32,
    y: Cell<u32>,
}

fn main() {
    let foo = Foo { x: 1, y: Cell::new(3)};
    assert_eq!(1, foo.x);
    assert_eq!(3, foo.y.get());
    foo.y.set(5);
    assert_eq!(5, foo.y.get());
}

Мы можем использовать методы set/get Cell для установки/получения внутренних значений. Это немного похоже на методы установки/получения, используемые в классах сущностей Java. Здесь следует отметить одну вещь:Cell<T>Завернутый T должен реализовать Copy, чтобы иметь возможность использовать метод get.Если Copy не реализован, вам нужно использовать метод get_mut, предоставленный Cell, чтобы вернуть изменяемое заимствование, а метод set можно использовать в любом случае. Видно, что Cell не нарушает правила заимствования.

Для типов, которые не реализуют копирование, используйтеCell<T>Все еще относительно неудобно, но, к счастью, Rust также предоставляетRefCell<T>. Без дальнейших церемоний, давайте перейдем непосредственно к коду.

use std::cell::RefCell;
fn main() {
    let x = RefCell::new(vec![1, 2, 3]);
    println!("{:?}", x.borrow());
    x.borrow_mut().push(5);
    println!("{:?}", x.borrow());
}

Из приведенного выше кода мы можем заметить, чтоRefCell<T>МетодызаимствованияизаимствованиясоответствуютCell<T>установить и получить методы в .

RefCell<T>а такжеCell<T>Еще одно отличие:Cell<T>Нет накладных расходов во время выполнения (но не оборачивайте ими большие структуры данных), в то время какRefCell<T>Существуют накладные расходы во время выполнения из-за использованияRefCell<T>Когда вам нужно поддерживать средство проверки заимствования, если правила заимствования нарушаются, это вызовет панику в потоке.

Суммировать

Сначала мы расскажем так много об интеллектуальных указателях, а теперь кратко их суммируем. Умные указатели Rust предоставляют нам множество полезных функций.Одной из особенностей умных указателей является то, что они реализуютDropа такжеDerefЭти две черты. вDropВ трейте предусмотрен метод drop, который будет вызываться при его уничтожении.DerefЧерты предоставляют возможность автоматического разыменования, поэтому нам не нужно разыменовывать вручную при использовании интеллектуальных указателей.

Затем мы вводим несколько общих интеллектуальных указателей.Box<T>может помочь нам выделить значения в куче памяти,Rc<T>Предоставляет нам возможность заимствовать несколько раз.RefCell<T>Сделайте внутреннюю изменчивость реальностью.

Последнее, но не менее важное, мы виделиStringа такжеVecТакже умный указатель.

Что касается того, почему они являются умными указателями, какие еще умные указатели предоставляет Rust? Давайте оставим здесь яму, и заинтересованные студенты могут наступить на нее сами.