Памятка по контейнерам – Ref, Box, Rc, Cell, Arc и т.д.

&T и &mut T или ref T и ref mut T

Это примитивный тип, который соответствует неизменяемым и изменяемым ссылкам соответственно. Мы можем иметь только одну изменяемую ссылку или любое количество неизменяемых ссылок для чтения, но не оба варианта одновременно. Эта гарантия применяется во время компиляции и не имеет видимых затрат во время выполнения. В большинстве случаев таких указателей достаточно для обмена дешевыми ссылками между разделами кода. Указатели не могут быть скопированы таким образом, чтобы они пережили данные на которые ссылаются.

Гарантии:

  • Гарантируется указание на валидные данные.
  • Есть время жизни.

Цена:

  • Нет

Автоматически реализация типажей:

  • Copy
  • Clone
  • Deref или DerefMut
  • Borrow или BorrowMut
  • Pointer

Пример:

1
2
3
4
5
6
7
8
9
10
11
12
13
use std::ptr;

let five = 5;
let other_five = 5;
let five_ref = &five;
let same_five_ref = &five;
let other_five_ref = &other_five;

assert!(five_ref == same_five_ref);
assert!(five_ref == other_five_ref);

assert!(ptr::eq(five_ref, same_five_ref));
assert!(!ptr::eq(five_ref, other_five_ref));

*const T и *mut T

Это C-подобные сырые указатели без привязки к ним времени жизни или владения. Они просто указывают на какое-то место в памяти. Других ограничений нет. Единственная гарантия, что они предоставляют, состоит в том, что они не могут быть разыменованы, за исключением места в коде, помеченного как небезопасный (unsafe). Эти типы могут быть полезны при создании безопасных и недорогих абстракций, таких как Vec <T>, но их следует избегать в безопасном коде.

  • не гарантируется указание на валидную память и даже не гарантируется, что она не равна нулю (в отличие от Box и &);
  • не имеют информации о владение, опять же в отличие от Box, поэтому компилятор Rust не может защитить от ошибок, таких как использование после освобождения;
  • считаются отправляемыми – если их внутреннее содержимое может быть отправляемым. Поэтому компилятор не предлагает никакой помощи для обеспечения потокобезопасности. Например, можно одновременно получить доступ к *mut i32 из двух потоков без синхронизации.
  • не имеют автоматической очистки, в отличие от Box, и поэтому требуют ручного управления ресурсами;
  • в отличие от &, у него нет времени жизни, поэтому компилятор не может рассуждать о висячих указателях. Также:
  • не имеют никаких гарантий относительно псевдонимов (aliasing) или мутабельности, кроме того, что мутация не допускается напрямую через *const T.

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

Фактически ссылка &T неявно приводится к сырому указателю *const T в безопасном коде. Аналогично для вариантов mut (оба приведения могут быть выполнены в явном виде value as *const T и value as *mut T).

Рекомендуемый метод для преобразования:

1
2
3
4
5
6
7
8
9
10
11
12
let i: u32 = 1;

// явное приведение
let p_imm: *const u32 = &i as *const u32;
let mut m: u32 = 2;

// неявное принуждение
let p_mut: *mut u32 = &mut m;
unsafe {
    let ref_imm: &u32 = &*p_imm;
    let ref_mut: &mut u32 = &mut *p_mut;
}

Стиль разыменования &*x предпочтительнее чем использование transmute, который имеет избыточные возможности для данной операции. Тем более разыменование &*x ограничивающая операция и её сложнее использовать не правильно. Требуется чтобы переменная x была указателем, тогда как для функции transmute – это не обязательно.

Гарантии:

  • Разыменование указателя доступно только в unsafe коде.

Цена:

  • Нет

Авто реализация типажей :

  • Send
  • Copy

Пример:

1
2
3
4
5
6
7
#![allow(unused)]
fn main() {
    let my_num: i32 = 10;
    let my_num_ptr: *const i32 = &my_num;
    let mut my_speed: i32 = 88;
    let my_speed_ptr: *mut i32 = &mut my_speed;
}

Box<T>

По умолчанию, все значения в Rust располагаются в стеке. «Владеющий указатель» или «упаковка». Может быть единственным владельцем данных. Данные помещаются на кучу. Может быть возвращена из функции. Когда упаковка оказывается за пределами области видимости, вызывается деструктор, содержащийся в ней объект уничтожается, а память в куче освобождается. Упакованные значения могут быть разыменованы с помощью операции *. Эта операция убирает один уровень косвенности.

Упаковка идеально подходит если вы хотите выделить память на куче и безопасно передать указатель на эту память. Обратите внимание, что вам будет разрешено делиться только ссылками по обычным правилам заимствования, которые проверяются во время компиляции. Перемещение владения это особенность всех типов, в которых не реализован типаж Copy. Большинство типов, содержащих указатели на другие данные не реализуют типаж Copy.

Почему не реализован Copy для упаковки?

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

Большинство типов содержащих указатели на другие данные не могут реализовать типаж Copy, поскольку некий тип лежащий на стеке может иметь дополнительные ссылки на данные и простое копирование лежащего на стеке типа может случайно разделить владение этими данными небезопасным образом. Т.е. такие типы как Vec<T> и String, которые имеют данные в куче, также не реализуют Copy. Тогда как integer и boolean реализуют Copy.

&T и сырые указатели реализуют типаж Copy. Даже если они указывают на дополнительные данные, они не «владеют» этими данными. Принимая во внимание, что Box<T> можно рассматривать как «некоторые данные, которые оказываются динамически распределяемыми», а &T – как «заимствование ссылки на некоторые данные». Хотя оба типа являются указателями, только упаковка считается «данными». Следовательно, копия упаковки должна включать в себя копию данных (которая не является частью ее стекового представления), но копия &T требует только копию ссылки. &mut T не является копией, потому что изменяемые псевдонимы не могут быть общими, и &mut T «владеет» данными, на которые он указывает, так как он может изменяться.

Практически говоря, тип может реализовать типаж Copy, если копия его стекового представления не нарушает безопасность памяти.

Гарантии:

Цена:

  • Нет

Авто реализация типажей:

  • Send
  • Sync

Пример:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
use std::mem;

#[allow(dead_code)]
#[derive(Debug, Clone, Copy)]
struct Point {
    x: f64,
    y: f64,
}

#[allow(dead_code)]
struct Rectangle {
    p1: Point,
    p2: Point,
}

fn origin() -> Point {
    Point { x: 0.0, y: 0.0 }
}

fn boxed_origin() -> Box<Point> {
    // Разместить эту точку в куче и вернуть указатель на неё
    Box::new(Point { x: 0.0, y: 0.0 })
}

fn main() {
    // (все аннотации типа избыточны)
    // Выделенные на стеке значения
    let point: Point = origin();
    let rectangle: Rectangle = Rectangle {
        p1: origin(),
        p2: Point { x: 3.0, y: 4.0 }
    };

    // Выделенный в куче квадрат
    let boxed_rectangle: Box<Rectangle> = Box::new(Rectangle {
        p1: origin(),
        p2: origin()
    });

    // Результат функции может быть упакован
    let boxed_point: Box<Point> = Box::new(origin());

    // Два уровня косвенной адресации
    let box_in_a_box: Box<Box<Point>> = Box::new(boxed_origin());

    println!("Point занимает {} байт в стеке",
             mem::size_of_val(&point));
    println!("Rectangle занимает {} байт в стеке",
             mem::size_of_val(&rectangle));

    // размер упаковки = размер указателя
    println!("Boxed point занимает {} байт в стеке",
             mem::size_of_val(&boxed_point));
    println!("Boxed rectangle занимает {} байт в стеке",
             mem::size_of_val(&boxed_rectangle));
    println!("Boxed box занимает {} байт в стеке",
             mem::size_of_val(&box_in_a_box));

    // Копировать данные, что находятся в `boxed_point`, в `unboxed_point`
    let unboxed_point: Point = *boxed_point;
    println!("Unboxed point занимает {} байт в стеке",
             mem::size_of_val(&unboxed_point));
}

Rc<T>

Rc<T> является указателем с подсчетом ссылок. Другими словами, это позволяет нам иметь несколько «владеющих» указателей на одни и те же данные, и данные будут освобождены (будут запущены деструкторы) только, когда все указатели находятся вне области видимости. Внутри он содержит общий «счетчик ссылок», который увеличивается каждый раз, когда Rc клонируется, и уменьшается каждый раз, когда одна из копий выходит из области видимости. Основная ответственность Rc<T> состоит в том, чтобы гарантировать, что деструкторы будут вызваны для общих данных.

Внутренние данные Rc не могут быть изменены, и если будут созданы перекрёстные ссылки, то произойдёт утечка данных. Это хорошо описано в стандартной документации в данном разделе.

Вообще, от таких утечек должен помогать сборщик мусора, но пока готовых решений нет.

Rc следует использовать, когда вы хотите динамически распределять и совместно использовать данные (только для чтения) между различными частями вашей программы. Когда не ясно, какая часть завершит использование указателя в последнюю очередь. Альтернатива &T, когда &T либо невозможно статически проверить на правильность, либо создает крайне неэргономичный код, в котором программист не хочет тратить затраты на разработку, работая с ним.

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

Есть похожий указатель – Weak<T>. Это не владеющий и не заимствующий умный указатель, который похож на &T, но без ограничения времени жизни. Weak<T> может сохраняться вечно. Однако, возможно, что попытка доступа к внутренним данным может завершиться неудачей и вернуть None, поскольку Weak пережил внутренние Rc. Это полезно для случаев, когда нужны циклические структуры данных и другие вещи.

Гарантии:

  • Основная гарантия заключается в том, что данные не будут уничтожены, пока все ссылки не будут удалены.

Цена:

  • Что касается памяти, Rc<T> – это одно выделение, хотя он выделит два дополнительных слова по сравнению с обычным Box<T> (для «сильных» и «слабых» счётчиков ссылок).
  • Rc<T> имеет вычислительную стоимость увеличения / уменьшения счетчика всякий раз, когда он клонируется или выходит из области видимости. Обратите внимание, что клон не будет делать полную копию, он просто увеличит внутренний счетчик ссылок и вернет копию Rc<T>.

Arc<T>

Arc<T> – это просто версия Rc<T>, которая использует атомарный счетчик ссылок (следовательно, «Arc»). Arc можно свободно отправить между потоками.

C_ shared_ptr в C++ похож на Arc, однако в случае C++ внутренние данные всегда изменчивы. Для семантики, аналогичной C++, мы должны использовать:

Arc<Mutex<T>>,
Arc<RwLock<T>>,
Arc<UnsafeCell<T>>

Последний следует использовать только в том случае, если вы уверены, что это не приведет к небезопасному использованию памяти. Помните, что запись в структуру не является атомарной операцией, и многие функции, такие как vec.push(), могут перераспределять внутренние данные и вызывать небезопасное поведение (поэтому даже монотонности может быть недостаточно для оправдания UnsafeCell).

Гарантии:

  • Как и в Rc – основная гарантия заключается в том, что данные не будут уничтожены, пока все ссылки не будут удалены.
  • Arc – потокобезопасен.

Цена:

  • По сравнению с Rc, есть ещё накладные расходы на атомарные операции с счётчиком ссылок.
  • Rc<T> имеет вычислительную стоимость увеличения / уменьшения счетчика всякий раз, когда он клонируется или выходит из области видимости. Обратите внимание, что клон не будет делать полную копию, он просто увеличит внутренний счетчик ссылок и вернет копию Rc<T>.

Weak<T>

Weak<T> – это версия Rc, которая содержит не владеющую ссылку на управляемое значение. Доступ к значению осуществляется путем вызова метода Weak<T>::upgrade, который возвращает Option<Rc<T>>.

Так как Weak<T> не имеет информации о владении, то она не будет препятствовать удалению внутреннего значения и не дает никаких гарантий относительно сохранения внутреннего значения и может вернуть None при вызове метода upgrade.

Слабый указатель полезен для хранения временной ссылки на значение в Rc без продления срока его службы. Он также используется для предотвращения циклических ссылок между указателями Rc, поскольку взаимные ссылки-владельцы никогда не позволят сбросить ни один Rc. Например, дерево может иметь сильные указатели Rc от родительских узлов до детей, а слабые указатели от детей обратно к их родителям.

Типичный способ получить слабый указатель – вызвать Rc::downgrade.


Cell<T>

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

Поскольку компилятор знает, что все данные, которыми владеет обертка находятся на стеке, то не стоит беспокоиться об утечке данных за ссылками (или, что еще хуже!), о простой замене данных.

С помощью этой обертки можно случайно изменить данные, поэтому будьте осторожны при ее использовании.

Данная обёртка – это хороший индикатор того, что данные можно изменять и что время между первым чтением и использованием этого значения будет минимальным.

Основные методы:

  • Метод get извлекает текущее внутреннее значение.
  • Метод take заменяет текущее внутреннее значение на Default::default() и возвращает заменил значение. Для типов, которые реализуют типаж Default.
  • Метод replace заменяет текущее внутреннее значение и возвращает замененное значение.
  • Метод into_inner использует Cell<T> и возвращает внутреннее значение.
  • Метод set заменяет внутреннее значение, дропая замененное значение.

Гарантии:

  • Это ослабляет ограничение «нет псевдонимов с изменчивостью» в тех местах, где это не нужно. Однако это также ослабляет гарантии, которые дает ограничение; поэтому, если чьи-то инварианты зависят от данных, хранящихся в ячейке, следует быть осторожным.
  • Это полезно для мутирования примитивов и других типов копирования, когда нет простого способа сделать это в соответствии со статическими правилами & и & mut.
  • Гарантии, предоставленные Cell довольно лаконичным образом:
    • Основная гарантия, которую мы должны гарантировать, заключается в том, что внутренние ссылки не могут быть признаны недействительными (висячие) путем мутации внешней структуры. (Подумайте о ссылках на внутренности типов, таких как Option, Box, Vec и т. Д.) &, & Mut и Cell, каждый из которых имеет свой собственный компромисс.
    • & разрешает общие внутренние ссылки, но запрещает мутации;
    • &mut разрешает мутацию xor внутренних ссылок, но не разделяет их;
    • Ячейка допускает общую изменчивость, но не внутренние ссылки.
  • В конечном счете, хотя общая изменчивость может вызвать много логических ошибок, она может вызвать ошибки безопасности памяти только в сочетании с «внутренними ссылками». Это для типов, у которых есть «интерьер», тип / размер которого можно изменить самостоятельно. Одним из примеров этого является перечисление Rust; где, изменив вариант, вы можете изменить, какой тип содержится. Если у вас есть псевдоним внутреннего типа во время изменения варианта, указатели в этом псевдониме могут быть недействительными.
  • Точно так же, если вы измените длину вектора, когда у вас есть псевдоним, к одному из его элементов, этот псевдоним может быть признан недействительным.
  • Поскольку Cell не допускает ссылки на внутреннюю часть типа (вы можете только копировать и копировать обратно), перечисления и структуры одинаково безопасны для псевдонимов.

Цена:

  • Использование Cell<T> не требует затрат времени выполнения, однако, если использовать его для переноса более крупных (Copy) структур, может быть целесообразно вместо этого обернуть отдельные поля в Cell<T>, поскольку каждая запись является полной копией структуры.

UnsafeCell<T>

Примитив для внутренней изменчивости в Rust. UnsafeCell <T> является типом, который оборачивает некоторые T и указывает на опасные внутренние операции над обернутым типом. Типы с полем UnsafeCell<T> считаются «небезопасными». Тип UnsafeCell<T> является единственным допустимым способом получения алиасов данных, которые считаются изменяемыми. Преобразование типа &T в &mut T считается неопределенным поведением.

RefCell<T>

Поскольку некоторый анализ невозможен, компилятор Rust не пытается даже что-либо предпринять. Если он не может быть уверен, поэтому он консервативен и иногда отвергает правильные которые фактически не нарушали бы гарантии Rust. Иными словами, если Rust пропускает неверную программу, люди не смогут доверять гарантиям Rust. Если Rust отклонит правильную программу, программист будет быть неудобным, но ничего катастрофического не может произойти. RefCell<T> полезен когда вы знаете, что правила заимствования соблюдаются, но компилятор не может понять, что это правильно.

Mutex<T>

RwLock<T>

Cow<T>

Pin<T>

Иногда может быть полезно запретить перемещение объекта, т.е. гарантировать неизменность его адреса в памяти. Основным сценарием использования такой возможности являются самоссылающиеся структуры, поскольку перемещение таких объектов приведет к инвалидации указателей, что может привести к неопределенному поведению (UB).

Pin<P> гарантирует, что объект, на который ссылается любой указатель типа P, имеет неизменное расположение в памяти, т.е. он не может быть перемещен и его память не может быть освобождена. Такие значения называются “закрепленными” (“pinned”).

Ожидается, что этот механизм будет использоваться в основном авторами библиотек, поэтому мы сейчас не станем погружаться глубже в детали (с которыми можно ознакомиться в документации по ссылке выше). Однако, стабилизация этого API является важным событием для всех пользователей Rust, потому что является ключевым этапом на пути к очень ожидаемому async/await. За статусом оставшейся работы в этом направлении можно следить на areweasyncyet.rs.


Box<Trait>

Arc<Trait>

Источники:

  1. https://manishearth.github.io/blog/2015/05/27/wrapper-types-in-rust-choosing-your-guarantees/
  2. https://github.com/cheblin/BlackBox/blob/master/Ref%2C%20Box%2C%20Rc%2C%20Cell%2C%20Arc2.pdf

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.