🧠 Управление памятью в Rust.
Часть 9. Box<T> smart pointer.
Мы говорили, что Stack – очень эффективная и производительная область памяти, поэтому Rust всегда старается разместить данные именно в этом сегменте, если это возможно (если размер данных известен на этапе компиляции и не меняется во время работы программы).
Однако существуют ситуации, когда данные могут быть сохранены в стеке, но разработчик хочет разместить их в Heap.
Например, чтобы избежать копирования большого количества данных при передаче их из функции в функцию.
Для этого нужно просто обернуть требуемую структуру в
В данном примере функция
При этом
Т.е. по результатам выполнения строки
в переменной data физически будет лежать адрес памяти типа usize (u32 на 32-битных и u64 на 64-битных процессорах), по которому будет размещён массив чисел.
А это в свою очередь означает, что значение переменной data будет сохранено в стеке функции main.
Если запутались, давайте ещё раз: сами данные массива лежат в Heap ([i32; 1000]), а ссылка на эти данные (Box<[i32; 1000]> или usize) – в Stack.
Таким образом, Box можно использовать ещё и тогда, когда нужно сохранить динамические данные в структурах, которые могут работать только с данными фиксированного размера.
Рассмотрим пример реализации полиморфизма в Rust:
Здесь я объявил две разные структуры (Dog и Cat), которые реализуют общее поведение Pet. И далее я хочу сохранить в вектор разные объекты с общим поведением Pet, чтобы вызывать их метод greet без приведения к конкретному типу.
Внутренняя структура динамического массива Vec устроена таким образом, что сохраняемые в неё объекты должны иметь одинаковый размер.
Структуры Dog и Cat обладают разным размером, но это можно обойти, обернув их в Box. В результате мы получим массив из указателей usize, по которым в дальнейшем данные будут динамически приведены к типу Pet для вызова метода greet.
Ещё один классический пример использования Box – создание структур, ссылающихся сами на себя (по типу связных списков):
Без использования Box Rust не может вычислить требуемый размер памяти под рекурсивную структуру, что приведёт к ошибке компиляции.
Использование Box помогает разорвать циклическую зависимость и превратить Page в структуру фиксированной длины.
#rust #rustlang #RustMemoryManagement #RustSmartPointers
Часть 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 параметров.
В таких ситуациях на выручку приходит умный указатель
Он позволяет работать с данными в разных местах программы, не создавая их копии.
Достигается это путём подсчёта ссылок на данные, сохранённые в Heap. Отсюда и название Rc – Reference Counter.
Передавая данные в какую-либо функцию или структуру, Rc увеличивает счётчик ссылок на 1. Когда переменные выходят из области видимости и должны быть очищены, Rc уменьшает счётчик на 1.
И лишь когда счётчик уменьшается до нуля, тогда происходит освобождение памяти, занимаемой данными.
Давайте рассмотрим пример.
Допустим, я хочу вести учёт электронных ключей, выдаваемых посетителям. Для доступа в каждую комнату нужен свой ключ.
В данном примере у меня есть три ключа, первые два я выдал посетителю с именем Bob, а третий – Alice.
Если я захочу выдать второй ключ и Бобу и Элис, то будет ошибка компиляции, т.к. нельзя передать владение над key_2 одновременно в два вектора.
Конкретно в этом маленьком примере можно было бы просто скопировать структуру Key.
Но если я не хочу клонировать данные, то можно воспользоваться умным указателем Rc:
Теперь у структуры Visitor хранится список совместно используемых ключей, а при создании вектора с ключами мы вызываем метод clone у Rc, тем самым увеличивая счётчик ссылок на каждый ключ.
Rc позволяет узнать сколько ссылок есть на структуру в определённый момент времени.
Давайте заменим объявление переменной v2 с посетителей Alice на такой код:
Сначала программа выведет число 3, так как на ключ для второй комнаты ведёт три ссылки: из переменной key_2, из вектора посетителя Боб и из вектора посетителя Элис.
После выхода Элис из скоупа, структура посетителя будет зачищена. При этом данные структуры key_2 останутся в памяти, а количество ссылок будет уменьшено до двух.
Продолжение ⬇️
#rust #rustlang #RustMemoryManagement #RustSmartPointers
Часть 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. Продолжение.
Потокобезопасная реализация работает медленнее, но в ситуациях, когда мы хотим получать доступ к общим ресурсам в разных потоках (например, получать доступ к соединению с БД в обработчиках эндпоинтов веб-сервиса), необходимо использовать именно Arc.
Стоит отметить, что обычно Rc и Arc предоставляют совместный доступ только к неизменяемым данным (immutable). То есть у структуры, обёрнутой в эти указатели, нельзя вызвать метод с ресивером &mut self.
Частично обойти это можно, воспользовавшись методом Rc::make_mut или Arc::make_mut, который вернёт mutable ссылку на данные.
Но это будет работать только тогда, когда на данные в текущий момент существует только одна ссылка. Если же на данные ссылается кто-то ещё, то при изменении они будут скопированы. Такой подход называется clone-on-write.
В общем же случае для изменения данных используются другие умные указатели (самостоятельно или совместно с Rc и Arc).
Но об этом мы поговорим в другой раз.
#rust #rustlang #RustMemoryManagement #RustSmartPointers
Часть 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.
Если оператор
Об этой операции и поговорим сегодня немного подробнее.
Рассмотрим простой пример:
Здесь я сначала сохраняю в переменную
Но что происходит с этими данными с точки зрения владения после разыменовывания ссылки?
Немного изменим пример:
Этот код не будет скомпилирован, Rust будет ругаться на последнюю строку, говоря, что при объявлении переменной
В целом это довольно логичное ограничение, так как в противном случае мы могли бы написать очень странный код: передав в функцию ссылку (а значит желая продолжать владеть данными), мы могли бы потерять владение над этими данными, если бы внутри функции кто-то бы разыменовал нашу ссылку:
⁉️ Зачем же тогда нужен dereference оператором?
Во-первых, оператор разыменования может быть использован с данными, помеченными трейтом
Поэтому самый первый пример с переменной типа
В примере с
Во-вторых, в Rust можно реализовать своё поведение для оператора
Это может быть удобно для работы со структурами-обёртками для получения доступа к внутренним данным:
Вызов
Более того, в Rust реализован механизм разыменованного приведения (ужасный перевод на русский 😅) или dereferece coercion – автоматического преобразования ссылки в нужный тип при передаче в функцию:
Благодаря реализованному трейту
Продолжение ⬇️
#rust #rustlang #RustStandardTraits #RustSmartPointers
Часть 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 – работу с умными указателями:
Благодаря реализованному в стандартной библиотеке трейту
И напоследок отмечу:
может показаться, что с помощью трейта
Однако это является антипаттерном.
По большей части трейты Deref/DerefMut предназначены только для удобства работы с внутренними данными различных структур-обёрток и умных указателей.
#rust #rustlang #RustStandardTraits #RustSmartPointers
Часть 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