Rusty Bytes – изучаем язык Rust
89 subscribers
1 photo
45 links
Канал про разработку на языке Rust: обучающие материалы, ссылки на полезные статьи, подборки интересных библиотек и фреймворков.
Download Telegram
🧠 Управление памятью в Rust.
Часть 9. Box<T> smart pointer.

std::boxed::Box<T> – пожалуй, самый простой умный указатель в Rust, при этом имеющий сразу несколько областей применения.

Мы говорили, что Stack – очень эффективная и производительная область памяти, поэтому Rust всегда старается разместить данные именно в этом сегменте, если это возможно (если размер данных известен на этапе компиляции и не меняется во время работы программы).

Однако существуют ситуации, когда данные могут быть сохранены в стеке, но разработчик хочет разместить их в Heap.
Например, чтобы избежать копирования большого количества данных при передаче их из функции в функцию.
Для этого нужно просто обернуть требуемую структуру в Box<T>:

fn init_arr() -> Box<[i32; 1000]> {
let arr = [1; 1000];
Box::new(arr)
}

fn sum_elements(arr: Box<[i32; 1000]>) -> i32 {
arr.iter().sum()
}

fn main() {
let data = init_arr();
println!("{}", sum_elements(data);
}


В данном примере функция init_arr инициализирует массив из тысячи единиц, обёрнутый в Box<[i32; 1000]>. Следовательно, этот массив будет сохранён в Heap, и при передаче в функцию main, а затем в функцию sum_elements, будет копироваться не сам массив данных, а ссылка на него.

При этом Box<T> не добавляет никакого оверхэда, а просто сохраняет данные в Heap, вместо Stack.
Т.е. по результатам выполнения строки

let data = init_arr();

в переменной data физически будет лежать адрес памяти типа usize (u32 на 32-битных и u64 на 64-битных процессорах), по которому будет размещён массив чисел.
А это в свою очередь означает, что значение переменной data будет сохранено в стеке функции main.

Если запутались, давайте ещё раз: сами данные массива лежат в Heap ([i32; 1000]), а ссылка на эти данные (Box<[i32; 1000]> или usize) – в Stack.

Таким образом, Box можно использовать ещё и тогда, когда нужно сохранить динамические данные в структурах, которые могут работать только с данными фиксированного размера.

Рассмотрим пример реализации полиморфизма в Rust:

struct Dog {
name: String,
age: u8
}

struct Cat {
name: String,
}

trait Pet {
fn greet(&self);
}

impl Pet for Dog {
fn greet(&self) {
println!("Woof! I'm {}.", self.name);
}
}

impl Pet for Cat {
fn greet(&self) {
println!("Meow! I'm {}.", self.name);
}
}

fn main() {
let pets: Vec<Box<dyn Pet>> = vec![
Box::new(Cat {
name: "Mittens".to_string(),
}),
Box::new(Dog {
name: "Rocket".to_string(),
}),
];
for pet in pets {
pet.greet();
}
}


Здесь я объявил две разные структуры (Dog и Cat), которые реализуют общее поведение Pet. И далее я хочу сохранить в вектор разные объекты с общим поведением Pet, чтобы вызывать их метод greet без приведения к конкретному типу.

Внутренняя структура динамического массива Vec устроена таким образом, что сохраняемые в неё объекты должны иметь одинаковый размер.
Структуры Dog и Cat обладают разным размером, но это можно обойти, обернув их в Box. В результате мы получим массив из указателей usize, по которым в дальнейшем данные будут динамически приведены к типу Pet для вызова метода greet.

Ещё один классический пример использования Box – создание структур, ссылающихся сами на себя (по типу связных списков):

struct Page {
page_data: String,
next_page: Option<Box<Page>>,
}

Без использования Box Rust не может вычислить требуемый размер памяти под рекурсивную структуру, что приведёт к ошибке компиляции.
Использование Box помогает разорвать циклическую зависимость и превратить Page в структуру фиксированной длины.

#rust #rustlang #RustMemoryManagement #RustSmartPointers
🧠 Управление памятью в Rust.
Часть 10.1. Rc<T> & Arc<T> smart pointers.

В процессе написания кода Rust разработчику нужно постоянно решать, кто будет владельцем тех или иных данных.
Удобнее всего работать с данными, которыми владеешь сам (owned data). Но это не всегда возможно.
Работа с заимствованными данными (borrowed data) может быть не всегда лёгкой и удобной, особенно когда дело доходит до lifetime параметров.

В таких ситуациях на выручку приходит умный указатель std::rc::Rc<T>, реализующий концепцию совместного владения (shared ownership).
Он позволяет работать с данными в разных местах программы, не создавая их копии.
Достигается это путём подсчёта ссылок на данные, сохранённые в Heap. Отсюда и название Rc – Reference Counter.

Передавая данные в какую-либо функцию или структуру, Rc увеличивает счётчик ссылок на 1. Когда переменные выходят из области видимости и должны быть очищены, Rc уменьшает счётчик на 1.
И лишь когда счётчик уменьшается до нуля, тогда происходит освобождение памяти, занимаемой данными.

Давайте рассмотрим пример.
Допустим, я хочу вести учёт электронных ключей, выдаваемых посетителям. Для доступа в каждую комнату нужен свой ключ.

struct Key {
room: u8,
}

struct Visitor {
name: String,
keys: Vec<Key>,
}

fn main() {
let (key_1, key_2, key_3) = (
Key { room: 1 },
Key { room: 2 },
Key { room: 3 }
);
let v1 = Visitor {
name: "Bob".to_string(),
keys: vec![key_1, key_2],
};
let v2 = Visitor {
name: "Alice".to_string(),
keys: vec![key_3],
};
}


В данном примере у меня есть три ключа, первые два я выдал посетителю с именем Bob, а третий – Alice.
Если я захочу выдать второй ключ и Бобу и Элис, то будет ошибка компиляции, т.к. нельзя передать владение над key_2 одновременно в два вектора.
Конкретно в этом маленьком примере можно было бы просто скопировать структуру Key.

Но если я не хочу клонировать данные, то можно воспользоваться умным указателем Rc:

use std::rc::Rc;

struct Key {
room: u8,
}

struct Visitor {
name: String,
keys: Vec<Rc<Key>>,
}

fn main() {
let (key_1, key_2, key_3) = (
Rc::new(Key { room: 1 }),
Rc::new(Key { room: 2 }),
Rc::new(Key { room: 3 }),
);
let v1 = Visitor {
name: "Bob".to_string(),
keys: vec![Rc::clone(&key_1), Rc::clone(&key_2)],
};
let v2 = Visitor {
name: "Alice".to_string(),
keys: vec![Rc::clone(&key_2), Rc::clone(&key_3)],
};
}


Теперь у структуры Visitor хранится список совместно используемых ключей, а при создании вектора с ключами мы вызываем метод clone у Rc, тем самым увеличивая счётчик ссылок на каждый ключ.

Rc позволяет узнать сколько ссылок есть на структуру в определённый момент времени.
Давайте заменим объявление переменной v2 с посетителей Alice на такой код:

{
let v2 = Visitor {
name: "Alice".to_string(),
keys: vec![Rc::clone(&key_2), Rc::clone(&key_3)],
};
println!("Key_2 references: {}", Rc::strong_count(&key_2));
}
println!("Key_2 references: {}", Rc::strong_count(&key_2));


Сначала программа выведет число 3, так как на ключ для второй комнаты ведёт три ссылки: из переменной key_2, из вектора посетителя Боб и из вектора посетителя Элис.
После выхода Элис из скоупа, структура посетителя будет зачищена. При этом данные структуры key_2 останутся в памяти, а количество ссылок будет уменьшено до двух.

Продолжение ⬇️

#rust #rustlang #RustMemoryManagement #RustSmartPointers
🧠 Управление памятью в Rust.
Часть 10.2. Rc<T> & Arc<T> smart pointers. Продолжение.

std::sync::Arc (Atomic Reference Counter) используется для тех же целей, что и Rc, но обладает потокобезопасной реализацией, в то время как Rc может использоваться только в однопоточной среде.

Потокобезопасная реализация работает медленнее, но в ситуациях, когда мы хотим получать доступ к общим ресурсам в разных потоках (например, получать доступ к соединению с БД в обработчиках эндпоинтов веб-сервиса), необходимо использовать именно Arc.

Стоит отметить, что обычно Rc и Arc предоставляют совместный доступ только к неизменяемым данным (immutable). То есть у структуры, обёрнутой в эти указатели, нельзя вызвать метод с ресивером &mut self.

Частично обойти это можно, воспользовавшись методом Rc::make_mut или Arc::make_mut, который вернёт mutable ссылку на данные.
Но это будет работать только тогда, когда на данные в текущий момент существует только одна ссылка. Если же на данные ссылается кто-то ещё, то при изменении они будут скопированы. Такой подход называется clone-on-write.

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

#rust #rustlang #RustMemoryManagement #RustSmartPointers
🧩 Стандартные трейты в Rust.
Часть 10.1. Deref & DerefMut.

Если оператор & используется для получения ссылки на данные, то оператор * нужен для обратной операции – разыменования ссылки (dereference), т.е. получения данных по ссылке.
Об этой операции и поговорим сегодня немного подробнее.

Рассмотрим простой пример:

fn main() {
let a = 1;
let b = &a;
let c = *b;
assert_eq!(a, c);
}

Здесь я сначала сохраняю в переменную b ссылку на данные переменной a, а затем в переменную c кладу данные, полученные по ссылке b (т.е. 1).

Но что происходит с этими данными с точки зрения владения после разыменовывания ссылки?
Немного изменим пример:

struct MyStruct(i32);

fn main() {
let a = MyStruct(1);
let b = &a;
let c = *b;
}

Этот код не будет скомпилирован, Rust будет ругаться на последнюю строку, говоря, что при объявлении переменной c, данные не могут быть перемещены из *b. Т.е. происходит попытка операции move, которая меняет владельца данных, но Rust запрещает это делать.

В целом это довольно логичное ограничение, так как в противном случае мы могли бы написать очень странный код: передав в функцию ссылку (а значит желая продолжать владеть данными), мы могли бы потерять владение над этими данными, если бы внутри функции кто-то бы разыменовал нашу ссылку:

struct MyStruct(i32);

fn main() {
let a = MyStruct(1);
incorrect_function(&a);
}

fn incorrect_function(param: &MyStruct) {
let a = *param; // ошибка компиляции
}


⁉️ Зачем же тогда нужен dereference оператором?

Во-первых, оператор разыменования может быть использован с данными, помеченными трейтом Copy. В этом случае при операции move вместо смены владения произойдёт копирование данных.

Поэтому самый первый пример с переменной типа i32 успешно компилируется: в переменной c будет копия данных переменной a.
В примере с MyStruct достаточно добавить к структуре атрибут #[derive(Copy)], чтобы код заработал.

Во-вторых, в Rust можно реализовать своё поведение для оператора * с помощью трейтов std::ops::Deref и std::ops::DerefMut:

pub trait Deref {
// Итоговый тип после разыменования.
type Target;
fn deref(&self) -> &Self::Target;
}

pub trait DerefMut: Deref {
// Т.к DerefMut расширяет Deref, то возвращаемый тип тот же самый.
fn deref_mut(&mut self) -> &mut Self::Target;
}


Это может быть удобно для работы со структурами-обёртками для получения доступа к внутренним данным:

struct Year(i32);

impl Deref for Year {
type Target = i32;

fn deref(&self) -> &Self::Target {
&self.0
}
}

fn main() {
let y = Year(2000);
assert_eq!(2000, *y);
}

Вызов *y в assert_eq фактически является синтаксическим сахаром для конструкции *(y.deref()), благодаря которой можно работать со структурой Year как с обычным числом.

Более того, в Rust реализован механизм разыменованного приведения (ужасный перевод на русский 😅) или dereferece coercion – автоматического преобразования ссылки в нужный тип при передаче в функцию:

fn main() {
let y = Year(2000);
print_next_year(&y);
print_next_int(&y);
}

fn print_next_year(year: &Year) {
println!("Next year: {}", year.0 + 1);
}

fn print_next_int(year: &i32) {
println!("Next int: {}", year + 1);
}

Благодаря реализованному трейту Deref, ссылку на Year можно передавать как в функцию, ожидающую &Year, так и в функцию, ожидающую &i32.

Продолжение ⬇️

#rust #rustlang #RustStandardTraits #RustSmartPointers
🧩 Стандартные трейты в Rust.
Часть 10.2. Deref & DerefMut. Продолжение.

Предыдущий пример может выглядеть довольно искусственными, поэтому посмотрим на самую полезную область применения dereference coercion – работу с умными указателями:

use std::ops::{Add, Deref};

struct Year(i32);

impl Year {
fn print_prev_year(&self) {
println!("Previous year: {}", self.0 - 1);
}
}

impl Deref for Year {
type Target = i32;

fn deref(&self) -> &Self::Target {
&self.0
}
}

fn main() {
let y = Box::new(Year(2000));
// можно передавать &Box<Year> в функцию, ожидающую &Year.
print_next_year(&y);
// можно вызывать методы Year напрямую
y.print_prev_year();
// можно вызывать методы i32 напрямую
println!("Year + 10 = {}", y.add(10));
}

fn print_next_year(year: &Year) {
println!("Next year: {}", year.0 + 1);
}


Благодаря реализованному в стандартной библиотеке трейту Deref для Box<T> (и для других умных указателей, таких как Rc, Arc и др.), работа со структурами, обёрнутыми в смарт-поинтеры, практически не добавляет дополнительного boilerplate кода.

DerefMut аналогичен трейту Deref, за исключением того, что позволяет работать с mutable ссылками.

И напоследок отмечу:
может показаться, что с помощью трейта Deref можно реализовать поведение, похожее на полиморфизм, когда одна структура как бы наследует другую и может быть передана в функцию, принимающая "родителя".
Однако это является антипаттерном.

По большей части трейты Deref/DerefMut предназначены только для удобства работы с внутренними данными различных структур-обёрток и умных указателей.

#rust #rustlang #RustStandardTraits #RustSmartPointers