Устроил себе эдакий «IT-детокс»: не занимаюсь ничем айтишным, стараюсь не читать технических статей, много времени провожу на свежем воздухе, занимаюсь всякими дачно-огородными делами.
Планирую в таком режиме отдыхать еще 1-2 недели, а затем буду потихоньку возвращаться к привычным делам и продолжу писать про Rust.
Впереди ещё много интересных тем!
Не теряйте.
🏝️
Планирую в таком режиме отдыхать еще 1-2 недели, а затем буду потихоньку возвращаться к привычным делам и продолжу писать про Rust.
Впереди ещё много интересных тем!
Не теряйте.
🏝️
👍7❤3🔥2
Забавно, что сообщение о том, что я не буду какое-то время писать посты, набрало больше всего положительных реакций среди всех публикаций канала.
Вдобавок ещё и количество подписчиков немного подросло.
Может, стоит почаще делать такие перерывы? 😅
Ну а если серьёзно, то планирую постепенно возвращаться к ведению канала.
Сегодня я просматриваю свою RSS ленту и скоро выложу пост со списком интересных на мой взгляд статей, вышедших за время моего отсутствия.
Ну и напоминаю (в том числе и себе), что у меня в самом разгаре две серии постов:
1️⃣ #RustMemoryManagement – публикации про управление памятью в языке Rust.
Мы уже рассмотрели такие важные понятия, как области памяти Stack и Heap, а также ключевые концепции Ownership, Borrowing и Lifetimes.
В ближайшее время планирую написать про ещё одну область памяти – статическую. А далее будем постепенно переходить к умным указателям (smart-pointers).
2️⃣ #RustStandardTraits – серия про трейты в стандартной библиотеке Rust.
Мы рассмотрели уже около 10-ти типажей, в основном касающихся конвертации данных.
Далее буду переходить к трейтам, относящимся к работе с памятью в Rust. Здесь как раз очень помогут публикации из предыдущей серии.
Разбавлять эти публикации как всегда буду ссылками на понравившиеся мне статьи и постами на какие-то общие темы.
Вдобавок ещё и количество подписчиков немного подросло.
Может, стоит почаще делать такие перерывы? 😅
Ну а если серьёзно, то планирую постепенно возвращаться к ведению канала.
Сегодня я просматриваю свою RSS ленту и скоро выложу пост со списком интересных на мой взгляд статей, вышедших за время моего отсутствия.
Ну и напоминаю (в том числе и себе), что у меня в самом разгаре две серии постов:
1️⃣ #RustMemoryManagement – публикации про управление памятью в языке Rust.
Мы уже рассмотрели такие важные понятия, как области памяти Stack и Heap, а также ключевые концепции Ownership, Borrowing и Lifetimes.
В ближайшее время планирую написать про ещё одну область памяти – статическую. А далее будем постепенно переходить к умным указателям (smart-pointers).
2️⃣ #RustStandardTraits – серия про трейты в стандартной библиотеке Rust.
Мы рассмотрели уже около 10-ти типажей, в основном касающихся конвертации данных.
Далее буду переходить к трейтам, относящимся к работе с памятью в Rust. Здесь как раз очень помогут публикации из предыдущей серии.
Разбавлять эти публикации как всегда буду ссылками на понравившиеся мне статьи и постами на какие-то общие темы.
❤1🔥1
За время моего отсутствия накопилось много всяческих статей, которыми хотелось бы поделиться.
Самое главное, что меня очень обрадовало – я вижу всё больше качественных публикаций по Rust на Хабре.
Отдельно хочется отметить блог компании Timeweb Cloud, в котором было опубликовано несколько хороших переводов зарубежных материалов:
🔹 Туториал по Tokio (часть 1, часть 2).
🔹 Rust Cookbook (часть 1, часть 2).
🔹 Практическое руководство по Rust (целых пять частей: ч.1, ч.2, ч.3, ч.4, ч.5).
Приятно видеть, когда помимо отдельных энтузиастов, компании тоже начинают вкладываться в популяризацию языка Rust, что помогает строить и развивать русскоязычное комьюнити, на базе которого в дальнейшем можно будет запускать всё больше и больше проектов на Rust.
Также хочу отметить туториалы в блоге компании OTUS:
🔹 Кратко про библиотеку Axum в Rust
🔹 Кратко про юнит-тесты в Rust
И перевод отличной статьи:
🔹 Почему я отказался от разработки игр на Rust (часть 1, часть 2, часть 3, часть 4)
Эту статью я прочитал в своё время в оригинале в блоге автора, и она мне очень понравилась. Да, в ней Rust больше критикуется, чем хвалится; с чем-то я согласен, с чем-то – нет.
Но всегда интересно почитать про чужой опыт и рассмотреть разные точки зрения, особенно если они так хорошо описаны.
Помимо этого в мае прошла конференция RustNL 2024, запись докладов можно найти на их YouTube-канале.
Сами доклады я ещё не смотрел, пробежался по темам и пока ничего для себя не отметил. Но может кому-то пригодится.
Ну и в конце несколько понравившихся мне статей на английском:
🔹 Parsing structured environment variables in Rust
🔹 Methods Should Be Object Safe
🔹 Error Handling for Large Rust Projects - A Deep Dive into GreptimeDB's Practices
🔹 Серия Build your own load balancer in Rust (Part 1, Part 2, Part 3)
🔹 Building a Redis / Kafka Data Sink
Самое главное, что меня очень обрадовало – я вижу всё больше качественных публикаций по Rust на Хабре.
Отдельно хочется отметить блог компании Timeweb Cloud, в котором было опубликовано несколько хороших переводов зарубежных материалов:
🔹 Туториал по Tokio (часть 1, часть 2).
🔹 Rust Cookbook (часть 1, часть 2).
🔹 Практическое руководство по Rust (целых пять частей: ч.1, ч.2, ч.3, ч.4, ч.5).
Приятно видеть, когда помимо отдельных энтузиастов, компании тоже начинают вкладываться в популяризацию языка Rust, что помогает строить и развивать русскоязычное комьюнити, на базе которого в дальнейшем можно будет запускать всё больше и больше проектов на Rust.
Также хочу отметить туториалы в блоге компании OTUS:
🔹 Кратко про библиотеку Axum в Rust
🔹 Кратко про юнит-тесты в Rust
И перевод отличной статьи:
🔹 Почему я отказался от разработки игр на Rust (часть 1, часть 2, часть 3, часть 4)
Эту статью я прочитал в своё время в оригинале в блоге автора, и она мне очень понравилась. Да, в ней Rust больше критикуется, чем хвалится; с чем-то я согласен, с чем-то – нет.
Но всегда интересно почитать про чужой опыт и рассмотреть разные точки зрения, особенно если они так хорошо описаны.
Помимо этого в мае прошла конференция RustNL 2024, запись докладов можно найти на их YouTube-канале.
Сами доклады я ещё не смотрел, пробежался по темам и пока ничего для себя не отметил. Но может кому-то пригодится.
Ну и в конце несколько понравившихся мне статей на английском:
🔹 Parsing structured environment variables in Rust
🔹 Methods Should Be Object Safe
🔹 Error Handling for Large Rust Projects - A Deep Dive into GreptimeDB's Practices
🔹 Серия Build your own load balancer in Rust (Part 1, Part 2, Part 3)
🔹 Building a Redis / Kafka Data Sink
🧩 Стандартные трейты в Rust.
Часть 9. Clone & Copy.
В процессе разработки на Rust встречаются ситуации, когда необходимо скопировать данные вместо их перемещения (операция move при смене владельца) или передачи на них ссылки (заимствования).
В первую очередь для таких целей используется трейт Clone:
Как видно, в данном типаже объявлено два метода, у одного из которых есть реализация по умолчанию: метод
А вот метод
Со ссылками тоже работает:
Таким образом Clone служит для реализации так называемого глубокого копирования (deep copy), когда копируются все внутренние поля, включая вложенные структуры.
Если структура реализует Clone, то дополнительно можно добавить ей реализацию типажа 🧩 Copy.
Это трейт-маркер, который можно "повесить" на структуру только через атрибут derive, причём только если все внутренние поля тоже реализуют типаж Copy.
Трейт Copy добавляет возможность неявного копирования структуры через операцию присваивания:
Copy реализует побитовое копирование, данное поведение не может быть переопределено.
Такое копирование более производительное, чем использование Clone или смена владельца (move). Поэтому зачастую для небольших простых типов данных (например, Value Objects) имеет смысл добавлять трейт Copy.
При этом структура, реализующая Copy, не может иметь реализацию трейта Drop.
В основном это связано с разным назначением данных типажей: Drop подразумевает наличие какого-то ресурса, который надо освобождать, а Copy – наоборот, предполагает наличие только простых данных, которые могут свободно копироваться, не заботясь о каком-либо состоянии.
#rust #rustlang #RustStandardTraits
Часть 9. Clone & Copy.
В процессе разработки на Rust встречаются ситуации, когда необходимо скопировать данные вместо их перемещения (операция move при смене владельца) или передачи на них ссылки (заимствования).
В первую очередь для таких целей используется трейт Clone:
pub trait Clone {
fn clone(&self) -> Self;
fn clone_from(&mut self, source: &Self) {
*self = source.clone()
}
}
Как видно, в данном типаже объявлено два метода, у одного из которых есть реализация по умолчанию: метод
clone_from()
вызывает внутри метод clone()
и является сокращённой записью для присваивания текущему объекту копии другого, т.е.:
a = b.clone();
// то же самое, что и
a.clone_from(&b);
А вот метод
clone()
нужно либо реализовывать самостоятельно, либо добавлять через атрибут derive, если все типы полей структуры уже реализуют трейт Clone:
#[derive(Clone, Debug)]
struct Color {
r: u8, g: u8, b: u8,
}
fn main() {
let red1 = Color { r: 255, g: 0, b: 0 };
let red2 = red1.clone();
println!("red1: {red1:?}\nred2: {red2:?}");
}
Со ссылками тоже работает:
#[derive(Clone)]
struct Person<'a> {
name: &'a str,
age: u32,
}
fn main() {
let person1 = Person { name: "Ян", age: 8 };
let person2 = person1.clone();
}
Таким образом Clone служит для реализации так называемого глубокого копирования (deep copy), когда копируются все внутренние поля, включая вложенные структуры.
Если структура реализует Clone, то дополнительно можно добавить ей реализацию типажа 🧩 Copy.
Это трейт-маркер, который можно "повесить" на структуру только через атрибут derive, причём только если все внутренние поля тоже реализуют типаж Copy.
Трейт Copy добавляет возможность неявного копирования структуры через операцию присваивания:
#[derive(Copy, Clone, Debug)]
struct Color {
r: u8,
g: u8,
b: u8,
}
fn main() {
let red1 = Color { r: 255, g: 0, b: 0 };
let red2 = red1; // тут произошло копирование, а не смена владельца
println!("red1: {red1:?}\nred2: {red2:?}");
}
Copy реализует побитовое копирование, данное поведение не может быть переопределено.
Такое копирование более производительное, чем использование Clone или смена владельца (move). Поэтому зачастую для небольших простых типов данных (например, Value Objects) имеет смысл добавлять трейт Copy.
При этом структура, реализующая Copy, не может иметь реализацию трейта Drop.
В основном это связано с разным назначением данных типажей: Drop подразумевает наличие какого-то ресурса, который надо освобождать, а Copy – наоборот, предполагает наличие только простых данных, которые могут свободно копироваться, не заботясь о каком-либо состоянии.
#rust #rustlang #RustStandardTraits
🧠 Управление памятью в Rust.
Часть 8.1 Static memory.
Помимо уже рассмотренных областей памяти Stack и Heap, в Rust есть ещё один сегмент памяти – статический (static).
Данная концепция скорее всего знакома вам по другим языкам программирования, тем не менее для полноты картины стоит затронуть эту тему.
Статическая область памяти – это память фиксированного размера, в которой при старте программы соответствующие данные инициализируются и живут на протяжении всего времени работы программы.
По аналогии со стеком, размер данных должен быть неизменным и известным на этапе компиляции программы, чтобы можно было рассчитать необходимый размер статического сегмента памяти.
В статическую память попадают константы, глобальные переменные, статические структуры, статические функции и различные метаданные, необходимые для работы программы.
Для инициализации данных в статической области памяти используется ключевое слово`static`.
Владение над статическими переменными не может быть передано, поэтому в функции можно передавать только ссылки. При этом для обозначения времени жизни подобных ссылок используется зарезервированный lifetime параметр
Обычно статические данные остаются неизменными на протяжении всей жизни программы, но если очень хочется, то можно объявить переменную как
Более безопасный вариант – использовать Atomic структуры:
Вообще, обычно не советуют увлекаться работой со статикой, так как использование глобальных переменных может негативно влиять на читаемость и структурированность программы, вдобавок осложняя написание юнит-тестов.
Но в некоторых случаях статические переменные могут быть очень полезны.
Например, при улучшении быстродействия программы.
Допустим, мы пишем высоконагруженный healthcheck сервис, который возвращает статус программы (HEALTHY / NOT_HEALTHY) и версию приложения.
Строковые значения статусов и версия – это статическая информация, а значит эти данные могут быть объявлены как константы.
В результате Rust не будет при каждом вызове создавать объекты, а затем их зачищать, а просто будет брать ссылку на статические данные.
Структура ответа может выглядеть как-то так:
Схожая концепция применяется и в языках с Garbage Collector для уменьшения количества генерируемого "мусора".
Продолжение ⬇️
#rust #rustlang #RustMemoryManagement
Часть 8.1 Static memory.
Помимо уже рассмотренных областей памяти Stack и Heap, в Rust есть ещё один сегмент памяти – статический (static).
Данная концепция скорее всего знакома вам по другим языкам программирования, тем не менее для полноты картины стоит затронуть эту тему.
Статическая область памяти – это память фиксированного размера, в которой при старте программы соответствующие данные инициализируются и живут на протяжении всего времени работы программы.
По аналогии со стеком, размер данных должен быть неизменным и известным на этапе компиляции программы, чтобы можно было рассчитать необходимый размер статического сегмента памяти.
В статическую память попадают константы, глобальные переменные, статические структуры, статические функции и различные метаданные, необходимые для работы программы.
Для инициализации данных в статической области памяти используется ключевое слово`static`.
Владение над статическими переменными не может быть передано, поэтому в функции можно передавать только ссылки. При этом для обозначения времени жизни подобных ссылок используется зарезервированный lifetime параметр
'static
:
struct StaticExample;
const CONST_EX: StaticExample = StaticExample {};
fn main() {
// Тип статической перемменной должен быть явно указан
static EX: StaticExample = StaticExample {};
// Такой вызов функции приведёт к ошибке компиляции
get_ownership(EX);
// А так передавать статические данные можыно
borrow(&EX);
// У констант тоже lifetime 'static
borrow(&CONST_EX);
}
fn get_ownership(ex: StaticExample) {}
fn borrow(ex: &'static StaticExample) {}
Обычно статические данные остаются неизменными на протяжении всей жизни программы, но если очень хочется, то можно объявить переменную как
mut static
и поменять её значение в блоке unsafe
.Более безопасный вариант – использовать Atomic структуры:
fn main() {
static_counter();
static_counter();
static_counter();
}
fn static_counter() {
static counter: AtomicU32 = AtomicU32::new(0);
println!(
"Кол-во вызовов функции: {}",ß
counter.fetch_add(1, Ordering::Relaxed) + 1
);
}
// Вывод программмы:
// Кол-во вызовов функции: 1
// Кол-во вызовов функции: 2
// Кол-во вызовов функции: 3
Вообще, обычно не советуют увлекаться работой со статикой, так как использование глобальных переменных может негативно влиять на читаемость и структурированность программы, вдобавок осложняя написание юнит-тестов.
Но в некоторых случаях статические переменные могут быть очень полезны.
Например, при улучшении быстродействия программы.
Допустим, мы пишем высоконагруженный healthcheck сервис, который возвращает статус программы (HEALTHY / NOT_HEALTHY) и версию приложения.
Строковые значения статусов и версия – это статическая информация, а значит эти данные могут быть объявлены как константы.
В результате Rust не будет при каждом вызове создавать объекты, а затем их зачищать, а просто будет брать ссылку на статические данные.
Структура ответа может выглядеть как-то так:
// версию берём из Cargo.toml
const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
const STATUS_OK: &str = "HEALTHY";
const STATUS_ERROR: &str = "NOT_HEALTHY";
struct HealthcheckResponse {
status: &'static str,
version: &'static str,
}
Схожая концепция применяется и в языках с Garbage Collector для уменьшения количества генерируемого "мусора".
Продолжение ⬇️
#rust #rustlang #RustMemoryManagement
🧠 Управление памятью в Rust.
Часть 8.2 Static memory. Продолжение.
Ещё один классический пример – вынос инициализации Regex выражений в статическую память.
Разбор и компиляция регулярного выражения – довольно тяжеловесная операция. Если мы пишем какой-нибудь веб-сервис, внутри которого собираемся валидировать или парсить данные по регулярке, то лучше её инциализировать один раз при старте программы.
Сделать это можно либо путём заведения структуры, внутри которой будет поле типа
❗️ Но, в отличие от примера с AtomicU32, такой код не скомпилируется.
Дело в том, что при инициализации статических переменных можно использовать только функции, помеченные ключевым словом
И если функция
Решаются подобные ситуации с помощью библиотеки lazy_static или её более современного аналога – once_cell. Эти библиотеки позволяют сделать отложенную (ленивую) инициализацию статических переменных:
Конечно же такой подход применим не только для работы с регулярными выражениями, но и для любой инициализации данных в статической области памяти уже после старта программы.
#rust #rustlang #RustMemoryManagement
Часть 8.2 Static memory. Продолжение.
Ещё один классический пример – вынос инициализации Regex выражений в статическую память.
Разбор и компиляция регулярного выражения – довольно тяжеловесная операция. Если мы пишем какой-нибудь веб-сервис, внутри которого собираемся валидировать или парсить данные по регулярке, то лучше её инциализировать один раз при старте программы.
Сделать это можно либо путём заведения структуры, внутри которой будет поле типа
Regex
(этот вариант сейчас рассматривать не будем), либо путём вынесения регулярки в статическое поле:
// для работы с регулярными выражениями в Rust используется библиотека regex
use regex::Regex;
fn verify_date() {
static RE: Regex = Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap();
assert!(RE.is_match("2010-03-14"));
}
❗️ Но, в отличие от примера с AtomicU32, такой код не скомпилируется.
Дело в том, что при инициализации статических переменных можно использовать только функции, помеченные ключевым словом
const
, так как результат их выполнения может быть вычислен на этапе компиляции.И если функция
new
для Atomic структур как раз является константной, то для Regex это не так.Решаются подобные ситуации с помощью библиотеки lazy_static или её более современного аналога – once_cell. Эти библиотеки позволяют сделать отложенную (ленивую) инициализацию статических переменных:
use once_cell::sync::Lazy;
use regex::Regex;
fn verify_date() {
static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap());
assert!(RE.is_match("2010-03-14"));
}
Конечно же такой подход применим не только для работы с регулярными выражениями, но и для любой инициализации данных в статической области памяти уже после старта программы.
#rust #rustlang #RustMemoryManagement
🧠 Управление памятью в 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
Изучая язык программирования, очень важно попытаться понять его идеологию.
Те ценности и ключевые концепции, которые разработчики хотели заложить в язык, во многом определяют реализацию тех или иных языковых конструкций и парадигм. И если понять (и принять) эти ценности, то становится гораздо проще не только изучить синтаксис, но и начать писать идиоматический код.
Нельзя сказать, что один ЯП лучше или хуже другого. Просто в каком-то языке делается упор на достижение каких-то конкретных целей (например, простоты синтаксиса, или максимальной гибкости, или возможности писать низкоуровневый код и т.п.). Но взамен приходится жертвовать чем-то другим, т.к. нельзя получить всё одновременно.
C++ хочет дать разработчику возможность делать всё, что он захочет, Go хочет быть языком с очень простым синтаксисом, Rust хочет быть безопасным при работе с памятью, Brainfuck был создан, чтобы выносить мозг, множество неизвестных языков, написанных в качестве хобби, были созданы с целью научиться чему-то новому.
В результате мне больше нравится оценивать ЯП с той точки зрения, насколько хорошо они следуют своим ценностям, консистентны ли они при реализации каждой из своих фич.
К примеру, Rust очень не любит всякие неявные штуки.
Он предпочитает более многословный синтаксис, порой готов предложить несколько схожих способов реализации какой-то задачи, отличающихся только тем, что они более точно позволяют отразить разные намерения программистов.
И каждое неявное преобразование добавлялось точечно с большой осторожностью.
Важно, чтобы разработчик хорошенько подумал какой вариант реализации выбрать.
Важно, чтобы код был однозначно понятен читателю.
Почему это важно?
Потому что в понятном коде меньше шансов допустить ошибку. Потому что в понятный код проще вносить изменения, не боясь его сломать. Потому что Rust хочет, чтобы на нём писали надёжные приложения на десятилетия вперёд.
Поэтому каждый раз, когда мне приходится явно преобразовывать
И тем удивительнее регулярно сталкиваться с трейтом Copy, который максимально неявным образом меняет подход работы со структурой (и с памятью) в зависимости от его наличия или отсутствия.
Не зная как выглядит структура Event, скажите, скомпилируется ли этот код?
¯\_(ツ)_/¯
Те ценности и ключевые концепции, которые разработчики хотели заложить в язык, во многом определяют реализацию тех или иных языковых конструкций и парадигм. И если понять (и принять) эти ценности, то становится гораздо проще не только изучить синтаксис, но и начать писать идиоматический код.
Нельзя сказать, что один ЯП лучше или хуже другого. Просто в каком-то языке делается упор на достижение каких-то конкретных целей (например, простоты синтаксиса, или максимальной гибкости, или возможности писать низкоуровневый код и т.п.). Но взамен приходится жертвовать чем-то другим, т.к. нельзя получить всё одновременно.
C++ хочет дать разработчику возможность делать всё, что он захочет, Go хочет быть языком с очень простым синтаксисом, Rust хочет быть безопасным при работе с памятью, Brainfuck был создан, чтобы выносить мозг, множество неизвестных языков, написанных в качестве хобби, были созданы с целью научиться чему-то новому.
В результате мне больше нравится оценивать ЯП с той точки зрения, насколько хорошо они следуют своим ценностям, консистентны ли они при реализации каждой из своих фич.
К примеру, Rust очень не любит всякие неявные штуки.
Он предпочитает более многословный синтаксис, порой готов предложить несколько схожих способов реализации какой-то задачи, отличающихся только тем, что они более точно позволяют отразить разные намерения программистов.
И каждое неявное преобразование добавлялось точечно с большой осторожностью.
Важно, чтобы разработчик хорошенько подумал какой вариант реализации выбрать.
Важно, чтобы код был однозначно понятен читателю.
Почему это важно?
Потому что в понятном коде меньше шансов допустить ошибку. Потому что в понятный код проще вносить изменения, не боясь его сломать. Потому что Rust хочет, чтобы на нём писали надёжные приложения на десятилетия вперёд.
Поэтому каждый раз, когда мне приходится явно преобразовывать
u8
в u32
, хотя Rust мог бы сделать это самостоятельно; каждый раз, когда для преобразования литеральной строки в String
мне приходится не просто это явно указывать, но и выбирать между String::from()
, .to_string()
, .to_owned()
, и .into()
; каждый раз когда я вижу трейты, лишь слегка отличающиеся друг от друга (например, Deref из прошлого и AsRef из следующего поста), я понимаю зачем и почему это сделано именно так.И тем удивительнее регулярно сталкиваться с трейтом Copy, который максимально неявным образом меняет подход работы со структурой (и с памятью) в зависимости от его наличия или отсутствия.
use some_external_crate::Event;
fn main() {
let event = Event::default();
save_event(event);
println!("saved event: {event:?}");
}
fn save_event(event: Event) {
// TBD
}
Не зная как выглядит структура Event, скажите, скомпилируется ли этот код?
¯\_(ツ)_/¯
❤2👍1
🧩 Стандартные трейты в Rust.
Часть 11.1. AsRef<T> & AsMut<T>.
Когда разработчик хочет создать какую-нибудь иерархию классов с помощью объектно-ориентированных языков программирования, обычно он выбирает между двумя подходами: наследованием и композицией.
Rust не поддерживает концепцию наследования, но с помощью структур и трейтов можно реализовать полноценную композицию.
Для примера возьмём такую иерархию структур:
🔹 Person, содержит персональную информацию о человеке;
🔹 User, содержит информацию о человеке и об его аккаунте (логин/пароль);
🔹 Tutor, является расширением структуры User с дополнительной информацией о темах, которым человек может обучать.
Допустим, мы хотим написать функцию, которая работает с данными Person, но вызывать её нужно будет для всех трёх структур (т.к. в некотором смысле все они – Person):
Здесь нам приходится самостоятельно указывать нужное внутреннее поле каждой сущности для передачи в функцию.
Это не очень хорошо как с технической, так и с концептуальной точки зрения.
Техническая проблема заключается в том, что далеко не всегда мы хотим делать внутренние поля структуры публичными (например, преследуя ещё один ООП принцип – инкапсуляцию), а значит к ним нельзя будет обратиться вне пакета, где они были объявлены.
Что касается концептуальной точки зрения, то изначально мы хотели в свою реализацию заложить мысль, что сущность User тоже является сущностью Person, просто расширенной. А Tutor в свою очередь – это и User и Person. То, каким образом соответствующие данные лежат в полях в каждой из структур, не должно особо волновать разработчика, использующего эти структуры.
Соответственно, мы хотим, что вызывающий код выглядел бы как-то так:
Именно для такой реализации нам и нужны трейты
Мы можем переписать функцию greet так, чтобы на вход она принимала
Маленький удручающий факт: для Person тоже придётся реализовывать
В результате мы получили возможность вызывать функцию, передавая наши структуры напрямую.
При этом для одной и той же структуры можно создать несколько реализаций AsRef с разными типами:
AsMut используется для тех же целей, что и AsRef, только возвращает mutable ссылку.
Продолжение ⬇️
#rust #rustlang #RustStandardTraits
Часть 11.1. AsRef<T> & AsMut<T>.
Когда разработчик хочет создать какую-нибудь иерархию классов с помощью объектно-ориентированных языков программирования, обычно он выбирает между двумя подходами: наследованием и композицией.
Rust не поддерживает концепцию наследования, но с помощью структур и трейтов можно реализовать полноценную композицию.
Для примера возьмём такую иерархию структур:
🔹 Person, содержит персональную информацию о человеке;
🔹 User, содержит информацию о человеке и об его аккаунте (логин/пароль);
🔹 Tutor, является расширением структуры User с дополнительной информацией о темах, которым человек может обучать.
struct Person {
name: String,
age: u8,
}
struct User {
person: Person,
login: String,
password: String,
}
struct Tutor {
user: User,
topics: Vec<String>,
}
Допустим, мы хотим написать функцию, которая работает с данными Person, но вызывать её нужно будет для всех трёх структур (т.к. в некотором смысле все они – Person):
fn greet(person: &Person) {
println!("Hi! I'm {}.", person.name);
}
fn main() {
let person = Person { ... };
let user = User { ... };
let tutor = Tutor { ... };
greet(&person);
greet(&user.person);
greet(&tutor.user.person);
}
Здесь нам приходится самостоятельно указывать нужное внутреннее поле каждой сущности для передачи в функцию.
Это не очень хорошо как с технической, так и с концептуальной точки зрения.
Техническая проблема заключается в том, что далеко не всегда мы хотим делать внутренние поля структуры публичными (например, преследуя ещё один ООП принцип – инкапсуляцию), а значит к ним нельзя будет обратиться вне пакета, где они были объявлены.
Что касается концептуальной точки зрения, то изначально мы хотели в свою реализацию заложить мысль, что сущность User тоже является сущностью Person, просто расширенной. А Tutor в свою очередь – это и User и Person. То, каким образом соответствующие данные лежат в полях в каждой из структур, не должно особо волновать разработчика, использующего эти структуры.
Соответственно, мы хотим, что вызывающий код выглядел бы как-то так:
greet(&person);
greet(&user);
greet(&tutor);
Именно для такой реализации нам и нужны трейты
std::convert::AsRef<T>
и std::convert::AsMut<T>
:
pub trait AsRef<T> {
fn as_ref(&self) -> &T;
}
pub trait AsMut<T> {
fn as_mut(&mut self) -> &mut T;
}
Мы можем переписать функцию greet так, чтобы на вход она принимала
AsRef<Person>
, и реализовать AsRef для User и Tutor:
impl AsRef<Person> for User {
fn as_ref(&self) -> &Person {
&self.person
}
}
impl AsRef<Person> for Tutor {
fn as_ref(&self) -> &Person {
&self.user.person
}
}
fn greet<P: AsRef<Person>>(person: P) {
println!("Hi! I'm {}.", person.as_ref().name);
}
Маленький удручающий факт: для Person тоже придётся реализовывать
AsRef<Person>
, в Rust нет generic реализации трейта для конвертации из T
в AsRef<T>
.
impl AsRef<Person> for Person {
fn as_ref(&self) -> &Person {
&self
}
}
В результате мы получили возможность вызывать функцию, передавая наши структуры напрямую.
При этом для одной и той же структуры можно создать несколько реализаций AsRef с разными типами:
// добавляем для Tutor и User конвертацию в AsRef<User>
impl AsRef<User> for User {
fn as_ref(&self) -> &User {
&self
}
}
impl AsRef<User> for Tutor {
fn as_ref(&self) -> &User {
&self.user
}
}
// и можем передавать обе структуры в функцию:
fn show_login<U: AsRef<User>>(user: U) {
println!("Login: {}", user.as_ref().login);
}
fn main() {
...
show_login(&user);
show_login(&tutor);
}
AsMut используется для тех же целей, что и AsRef, только возвращает mutable ссылку.
Продолжение ⬇️
#rust #rustlang #RustStandardTraits
🧩 Стандартные трейты в Rust.
Часть 11.2. AsRef<T> & AsMut<T>. Продолжение.
AsRef/AsMut широко распространены в стандартной библиотеке Rust.
Например, тип String – это обёртка над динамическим массивом из ASCII символов (`Vec<u8>`). И если выставлять наружу этот вектор нельзя, чтобы разработчики чего-нибудь не сломали при работе с ним напрямую, то позволить использовать String как массив символов – можно.
Поэтому для String реализован
Также использование
Если взять в качестве примера те же строки, то мы можем объявить функцию так:
и на вход ей можно будет передать типы
либо объявить функцию так:
и теперь в функцию дополнительно к
⚠️ Если вы разрабатываете библиотеку на Rust, стоит хорошенько подумать, какие реализации AsRef/AsMut могут пригодиться для ваших типов, и в каких функциях лучше принимать на вход AsRef/AsMut вместо конкретных типов.
Это повысит DX (Developer Experience) при использовании вашей библиотеки и добавит плюсиков в карму 😊
Напоследок давайте ещё раз вернёмся к трейту Deref, сравним его с AsRef и подытожим в каких случаях какой типаж лучше использовать.
Можно отметить два основных отличия с технической точки зрения:
🔸 Deref можно реализовать только один раз для одной структуры. AsRef можно реализовать несколько раз для преобразования структуры в разные типы.
🔸 При использовании Deref в Rust реализовано автоматическое разыменование ссылки в конкретный тип, в то время как AsRef используется в явноном виде.
Отсюда следуют идеологические отличия:
❗️ Deref лучше использовать тогда, когда родительская структура не имеет какого-то бизнес-смысла, а является необходимостью для достижения какой-то технической цели.
Именно поэтому классическим примером, иллюстрирующим работу Deref, являются умные указатели. Сами по себе они не несут какого-то дополнительного смысла, поэтому их использование должно быть как можно более прозрачным: если мы хотим принимать в функции какой-то тип, не важно будет ли передаваться в него чистый тип, или обёрнутый, к примеру, в
Ещё один хороший пример для использования Deref – структуры-обёртки.
Например, мы хотим создать тип AccountId, который является обёрткой для типа Uuid:
Это может быть полезно, чтобы реализовать какие-нибудь дополнительные трейты или методы для работы с данным типом.
При этом могут существовать какие-то другие функции, принимающие Uuid, которые прекрасно могут работать и с AccountId.
Чтобы добавить прозрачную конвертацию из AccountId в Uuid, можно для этой структуры реализовать трейт Deref, который упростит использование AccountId и подчеркнёт суть этой структуры как простой обёртки.
❗️ Если же AccountId задумывается как самостоятельная структура с идентификационными данными аккаунта, которая может включать в себя какие-то другие атрибуты, кроме Uuid, то лучше для неё реализовать AsRef, даже если поле с Uuid на данный момент является единственным атрибутом структуры.
Тогда в дальнейшем, когда в AccountId будут добавлены другие поля с данными, например, Passport, можно будет дописать ещё одну реализацию AsRef, позволяя использовать сущность AccountId и как Uuid, и как Passport.
Другими словами, если мы реализовали иерархию сущностей через композицию и хотим предоставлять одну сущность как другую (User как Person, Tutor как User или как Person, AccountId как Passport), то AsRef – то, что нужно.
#rust #rustlang #RustStandardTraits
Часть 11.2. AsRef<T> & AsMut<T>. Продолжение.
AsRef/AsMut широко распространены в стандартной библиотеке Rust.
Например, тип String – это обёртка над динамическим массивом из ASCII символов (`Vec<u8>`). И если выставлять наружу этот вектор нельзя, чтобы разработчики чего-нибудь не сломали при работе с ним напрямую, то позволить использовать String как массив символов – можно.
Поэтому для String реализован
AsRef<[u8]>
.Также использование
AsRef
при объявлении типов входящих параметров может расширить перечень принимаемых типов, а значит немного упростить жизнь разработчику, использующему эту функцию.Если взять в качестве примера те же строки, то мы можем объявить функцию так:
fn takes_str(s: &str) {
// используем &str
}
и на вход ей можно будет передать типы
&str
и &String
,либо объявить функцию так:
fn takes_asref_str<S: AsRef<str>>(s: S) {
let s: &str = s.as_ref();
// используем &str
}
и теперь в функцию дополнительно к
&str
и &String
можно передавать тип String
, т.к. для него уже реализован трейт AsRef<str>
.⚠️ Если вы разрабатываете библиотеку на Rust, стоит хорошенько подумать, какие реализации AsRef/AsMut могут пригодиться для ваших типов, и в каких функциях лучше принимать на вход AsRef/AsMut вместо конкретных типов.
Это повысит DX (Developer Experience) при использовании вашей библиотеки и добавит плюсиков в карму 😊
Напоследок давайте ещё раз вернёмся к трейту Deref, сравним его с AsRef и подытожим в каких случаях какой типаж лучше использовать.
Можно отметить два основных отличия с технической точки зрения:
🔸 Deref можно реализовать только один раз для одной структуры. AsRef можно реализовать несколько раз для преобразования структуры в разные типы.
🔸 При использовании Deref в Rust реализовано автоматическое разыменование ссылки в конкретный тип, в то время как AsRef используется в явноном виде.
Отсюда следуют идеологические отличия:
❗️ Deref лучше использовать тогда, когда родительская структура не имеет какого-то бизнес-смысла, а является необходимостью для достижения какой-то технической цели.
Именно поэтому классическим примером, иллюстрирующим работу Deref, являются умные указатели. Сами по себе они не несут какого-то дополнительного смысла, поэтому их использование должно быть как можно более прозрачным: если мы хотим принимать в функции какой-то тип, не важно будет ли передаваться в него чистый тип, или обёрнутый, к примеру, в
Box<T>
. Ещё один хороший пример для использования Deref – структуры-обёртки.
Например, мы хотим создать тип AccountId, который является обёрткой для типа Uuid:
struct AccountId(uuid::Uuid);
Это может быть полезно, чтобы реализовать какие-нибудь дополнительные трейты или методы для работы с данным типом.
При этом могут существовать какие-то другие функции, принимающие Uuid, которые прекрасно могут работать и с AccountId.
Чтобы добавить прозрачную конвертацию из AccountId в Uuid, можно для этой структуры реализовать трейт Deref, который упростит использование AccountId и подчеркнёт суть этой структуры как простой обёртки.
❗️ Если же AccountId задумывается как самостоятельная структура с идентификационными данными аккаунта, которая может включать в себя какие-то другие атрибуты, кроме Uuid, то лучше для неё реализовать AsRef, даже если поле с Uuid на данный момент является единственным атрибутом структуры.
Тогда в дальнейшем, когда в AccountId будут добавлены другие поля с данными, например, Passport, можно будет дописать ещё одну реализацию AsRef, позволяя использовать сущность AccountId и как Uuid, и как Passport.
Другими словами, если мы реализовали иерархию сущностей через композицию и хотим предоставлять одну сущность как другую (User как Person, Tutor как User или как Person, AccountId как Passport), то AsRef – то, что нужно.
#rust #rustlang #RustStandardTraits
📚 Небольшая пятничная подборка статей, показавшихся мне достаточно интересными.
📄 The ultimate guide to Rust newtypes
Паттерн newtype очень распространён при разработке на языке Rust.
В своих постах я регулярно привожу примеры с использованием newtype, обычно называя такие структуры обёртками.
В статье очень подробно описывается данный паттерн с хорошими примерами.
📄 How to organize large Rust codebases
Правильная организация кода по файлам и директориям в проекте важна не меньше, чем написание красивого кода, если на выходе вы хотите получить понятное и простое в разработке и поддержке приложение.
В данной статьей автор приводит полюбившийся ему подход, с которым я по большей части согласен.
📄 Master hexagonal architecture in Rust
Ещё одна статья про организацию кода, только теперь с точки зрения архитектуры и проектирования. Если хотите посмотреть на то, как реализуется гексагональная архитектура в Rust приложении или если вы вообще не знакомы с термином гексагональной архитектуры, эта статья должна вам понравиться.
📄 Demystifying Rust's HTTP ecosystem: Here is how the different crates fit together
Я как-то писал пост, в котором постарался разложить по полочкам для чего нужны популярные http-библиотеки в Rust и как они взаимосвязаны между собой. В этой статье написано примерно про то же самое, только чуть более детально и с наглядной картинкой.
📄 Writing production Rust macros with macro_rules!
Очень большой труд, помогающий понять и научиться писать макросы в Rust с нуля на примере существующих макросов.
📄 Playing guitar tablatures in Rust
Интересная статья про то, как автор писал инструмент для проигрывания гитарных табулатур.
Можно использовать в качестве напоминания (и вдохновения) о том, что писать можно не только какие-то серьёзные корпоративные приложения, но и вот такие вещи для души.
📄 The ultimate guide to Rust newtypes
Паттерн newtype очень распространён при разработке на языке Rust.
В своих постах я регулярно привожу примеры с использованием newtype, обычно называя такие структуры обёртками.
В статье очень подробно описывается данный паттерн с хорошими примерами.
📄 How to organize large Rust codebases
Правильная организация кода по файлам и директориям в проекте важна не меньше, чем написание красивого кода, если на выходе вы хотите получить понятное и простое в разработке и поддержке приложение.
В данной статьей автор приводит полюбившийся ему подход, с которым я по большей части согласен.
📄 Master hexagonal architecture in Rust
Ещё одна статья про организацию кода, только теперь с точки зрения архитектуры и проектирования. Если хотите посмотреть на то, как реализуется гексагональная архитектура в Rust приложении или если вы вообще не знакомы с термином гексагональной архитектуры, эта статья должна вам понравиться.
📄 Demystifying Rust's HTTP ecosystem: Here is how the different crates fit together
Я как-то писал пост, в котором постарался разложить по полочкам для чего нужны популярные http-библиотеки в Rust и как они взаимосвязаны между собой. В этой статье написано примерно про то же самое, только чуть более детально и с наглядной картинкой.
📄 Writing production Rust macros with macro_rules!
Очень большой труд, помогающий понять и научиться писать макросы в Rust с нуля на примере существующих макросов.
📄 Playing guitar tablatures in Rust
Интересная статья про то, как автор писал инструмент для проигрывания гитарных табулатур.
Можно использовать в качестве напоминания (и вдохновения) о том, что писать можно не только какие-то серьёзные корпоративные приложения, но и вот такие вещи для души.
🧠 Управление памятью в Rust.
Часть 12.1. RefCell<T>.
Когда мы рассматривали концепцию Borrowing, я упоминал про правило заимствования, которое Rust проверяет при компиляции: в один момент времени может существовать либо сколько угодно ссылок на чтение данных, либо только одна ссылка на изменение этих данных.
Это помогает избежать некоторых проблем при работе с памятью, например, защищает от обращения к уже очищенной области памяти.
Такой подход, когда мы получаем ссылки на данные для чтения или записи, а корректность работы с ними проверяется на этапе компиляции, называется статическое заимствование.
В Rust также существует динамическое заимствование – подход, при котором мы заворачиваем данные в контейнер
RefCell реализует методы borrow/borrow_mut для получения ссылок для чтения/изменения данных, и если попробовать получить одновременно две mutable ссылки или поработать со ссылкой на чтение, которая была получена до работы со ссылкой на изменение, Rust выбросит runtime ошибку – panic.
⁉️ Зачем может понадобиться такое поведение?
Ключевая ценность RefCell кроется в его методе borrow_mut (или try_borrow_mut, который возвращает Result, чтобы можно было избежать паники). Несмотря на то, что метод возвращает мутабельную ссылку на объект, ресивер у него –
Это значит, что с помощью RefCell можно менять объект внутри метода, который не требует ссылки для изменения.
А раз нет ссылок для изменения, то и по поводу правила заимствования можно не переживать – все ссылки только для чтения, и таких ссылок одновременно можно получать сколько угодно;
Такая концепция называется внутренняя изменчивость (interior mutability): объект можно изменять через ссылку
💡 Давайте разбираться на примере.
Допустим, мы хотим реализовать такую структуру-контейнер, которая бы внутри хранила в себе какие-то данные типа T.
И допустим, мы хотим внутри этой структуры объявить boolean флаг, показывающий обращались ли к данным внутри Holder через метод get хотя бы один раз или нет.
Проблема такого решения в том, что нам пришлось объявить ресивер у метода get как
А это плохо как с точки зрения чтения и понимания кода (зачем методу get для чтения данных мутабельная ссылка?), так и с точки зрения использования Holder-а: придётся учитывать правила заимствования, ограничивающие количество мутабельных ссылок.
В таких ситуациях на помощь приходит interior mutability и RefCell в частности:
В данном случае мы можем воспользоваться методом
Продолжение ⬇️
#rust #rustlang #RustMemoryManagement
Часть 12.1. RefCell<T>.
Когда мы рассматривали концепцию Borrowing, я упоминал про правило заимствования, которое Rust проверяет при компиляции: в один момент времени может существовать либо сколько угодно ссылок на чтение данных, либо только одна ссылка на изменение этих данных.
Это помогает избежать некоторых проблем при работе с памятью, например, защищает от обращения к уже очищенной области памяти.
Такой подход, когда мы получаем ссылки на данные для чтения или записи, а корректность работы с ними проверяется на этапе компиляции, называется статическое заимствование.
В Rust также существует динамическое заимствование – подход, при котором мы заворачиваем данные в контейнер
std::cell::RefCell
, а корректность работы со ссылками на эти данные проверяется в процессе работы программы (в райнтайме).RefCell реализует методы borrow/borrow_mut для получения ссылок для чтения/изменения данных, и если попробовать получить одновременно две mutable ссылки или поработать со ссылкой на чтение, которая была получена до работы со ссылкой на изменение, Rust выбросит runtime ошибку – panic.
⁉️ Зачем может понадобиться такое поведение?
Ключевая ценность RefCell кроется в его методе borrow_mut (или try_borrow_mut, который возвращает Result, чтобы можно было избежать паники). Несмотря на то, что метод возвращает мутабельную ссылку на объект, ресивер у него –
&self
, а не &mut self
.Это значит, что с помощью RefCell можно менять объект внутри метода, который не требует ссылки для изменения.
А раз нет ссылок для изменения, то и по поводу правила заимствования можно не переживать – все ссылки только для чтения, и таких ссылок одновременно можно получать сколько угодно;
Такая концепция называется внутренняя изменчивость (interior mutability): объект можно изменять через ссылку
&T
, а не через &mut T
. Изменения через &mut T
называют наследуемой изменчивостью (inherited mutability).💡 Давайте разбираться на примере.
Допустим, мы хотим реализовать такую структуру-контейнер, которая бы внутри хранила в себе какие-то данные типа T.
И допустим, мы хотим внутри этой структуры объявить boolean флаг, показывающий обращались ли к данным внутри Holder через метод get хотя бы один раз или нет.
struct Holder<T> {
data: T,
touched: bool
}
impl<T> Holder<T> {
fn new(init: T) -> Self {
Holder { data: init, touched: false }
}
fn get(&mut self) -> &T {
self.touched = true;
&self.data
}
}
Проблема такого решения в том, что нам пришлось объявить ресивер у метода get как
&mut self
, а не как &self
.А это плохо как с точки зрения чтения и понимания кода (зачем методу get для чтения данных мутабельная ссылка?), так и с точки зрения использования Holder-а: придётся учитывать правила заимствования, ограничивающие количество мутабельных ссылок.
В таких ситуациях на помощь приходит interior mutability и RefCell в частности:
struct Holder<T> {
data: T,
touched: RefCell<bool>,
}
impl<T> Holder<T> {
fn new(init: T) -> Self {
Holder {
data: init,
touched: RefCell::new(false),
}
}
fn get(&self) -> &T {
self.touched.replace(true);
&self.data
}
}
В данном случае мы можем воспользоваться методом
replace
, который внутри вызывает borrow_mut
, а метод get может быть объявлен с ресивером &self
, не накладывая дополнительных проблем на его использование.Продолжение ⬇️
#rust #rustlang #RustMemoryManagement
🧠 Управление памятью в Rust.
Часть 12.2. RefCell<T>. Продолжение.
Свойство interior mutability, позволяющее изменять данные там, где не предполагается их изменение, хорошо сочетается с умным указателем Rc (Reference Counter).
В прошлой части я упоминал, что Rc предоставляет совместный доступ к данным, которые нельзя изменить, т.к. указатель не даёт вызывать методы с ресивером &mut self.
Но если скомбинировать Rc с RefCell, то это ограничение можно обойти:
❗️ Стоит отметить, что контейнер RefCell не является потокобезопасным, поэтому его нельзя использовать совместно с Arc (Atomic Reference Counter).
Для работы в многопоточной среде вместе с Arc обычно используется RwLock (Read-Write Lock), который тоже позволяет в один момент времени либо читать данные любому количеству потоков, либо изменять данные кому-то одному.
Подводя итог, скажу, что RefCell не стоит использовать просто так, чтобы обойти ограничения компилятора.
Сами правила работы со ссылками никуда не деваются, просто вы их перенесёте с этапа компиляции в рантайм, и приложение всё равно не будет работать.
RefCell позволяет решать ограниченный набор вполне конкретных задач. В остальных ситуациях лучше использовать стандартную концепцию наследуемой изменчивости и дать компилятору сделать свою работу и помочь вам избежать ошибок.
#rust #rustlang #RustMemoryManagement
Часть 12.2. RefCell<T>. Продолжение.
Свойство interior mutability, позволяющее изменять данные там, где не предполагается их изменение, хорошо сочетается с умным указателем Rc (Reference Counter).
В прошлой части я упоминал, что Rc предоставляет совместный доступ к данным, которые нельзя изменить, т.к. указатель не даёт вызывать методы с ресивером &mut self.
Но если скомбинировать Rc с RefCell, то это ограничение можно обойти:
let v1 = Rc::new(vec![1]);
// v1.push(2); // так нельзя
let v2 = Rc::new(RefCell::new(vec![1]));
v2.borrow_mut().push(2); // а так можно!
❗️ Стоит отметить, что контейнер RefCell не является потокобезопасным, поэтому его нельзя использовать совместно с Arc (Atomic Reference Counter).
Для работы в многопоточной среде вместе с Arc обычно используется RwLock (Read-Write Lock), который тоже позволяет в один момент времени либо читать данные любому количеству потоков, либо изменять данные кому-то одному.
Подводя итог, скажу, что RefCell не стоит использовать просто так, чтобы обойти ограничения компилятора.
Сами правила работы со ссылками никуда не деваются, просто вы их перенесёте с этапа компиляции в рантайм, и приложение всё равно не будет работать.
RefCell позволяет решать ограниченный набор вполне конкретных задач. В остальных ситуациях лучше использовать стандартную концепцию наследуемой изменчивости и дать компилятору сделать свою работу и помочь вам избежать ошибок.
#rust #rustlang #RustMemoryManagement
🧠 Управление памятью в Rust.
Часть 13. Cell<T>, OnceCell<T>, LazyCell<T, F>.
Помимо контейнера
Рассмотрим их чуть более подробно.
1️⃣
В то время как
⁉️ Зачем может понадобиться копирование вместо получения ссылки?
Я упоминал, что RefCell при обращении к внутреннему объекту выполняет проверки заимствования, по итогам которых может выбросить runtime ошибку. Эта операция не бесплатна и требует некоторых вычислительных ресурсов и времени.
При копировании объекта выполнять такие проверки не требуется, поэтому в некоторых случаях такой подход будет более производительным.
Здесь логика очень схожа с тем, как объекты передаются из метода в метод: большие и сложные структуры лучше передавать по ссылке, а вот простые типы будет быстрее скопировать.
Для получения копии из контейнера Cell с помощью метода get внутренний тип должен реализовывать трейт Copy.
Второй вариант: тип может реализовывать трейт Default. Тогда вы можете вызвать метод
Таким образом, в прошлом примере, где я использовал RefCell для изменения флага boolean, лучше подошёл бы Cell.
2️⃣
В Rust версии 1.70 добавился новый контейнер –
Это можно сделать либо с помощью метода set (самостоятельно проконтролировав, что метод будет вызван только один раз), либо с помощью метода get_or_init, в который передаётся функция инициализации (она будет вызвана только при первом обращении к контейнеру).
Примеры можно посмотреть в официальной документации.
Ключевым преимуществом OnceCell является то, что он возвращает ссылку на внутренний объект (как RefCell), но не выполняет runtime проверок правил заимствования.
Реализация
3️⃣
Контейнер
LazyCell схож с OnceCell по смыслу и назначению, но позволяет указать функцию инициализации один раз при создании контейнера, вместо того, чтобы указывать её каждый раз в методe get_or_init, как для OnceCell. При этом данная функция тоже будет вызвана один раз при первом обращении к контейнеру для получения ссылки на объект.
Реализация
С появлением контейнеров OnceCell и LazyCell в стандартной библиотеке языка необходимость в таких библиотеках, как lazy_static или once_cell, практически отпала.
Эти библиотеки я упоминал в посте про статическую память, там же есть пример их использования, который теперь можно реализовать только с помощью OnceCel/LazyCell.
#rust #rustlang #RustMemoryManagement
Часть 13. Cell<T>, OnceCell<T>, LazyCell<T, F>.
Помимо контейнера
RefCell
, который мы рассмотрели в предыдущем посте, пакет std::cell
может предложить ещё несколько видов контейнеров для реализации внутренней изменчивости (interior mutability).Рассмотрим их чуть более подробно.
1️⃣
Cell<T>
В то время как
RefCell
позволяет получить мутабельную ссылку на объект, std::cell::Cell
позволяет получить копию внутреннего объекта, которую можно изменить и положить обратно в Cell вместо старой версии.⁉️ Зачем может понадобиться копирование вместо получения ссылки?
Я упоминал, что RefCell при обращении к внутреннему объекту выполняет проверки заимствования, по итогам которых может выбросить runtime ошибку. Эта операция не бесплатна и требует некоторых вычислительных ресурсов и времени.
При копировании объекта выполнять такие проверки не требуется, поэтому в некоторых случаях такой подход будет более производительным.
Здесь логика очень схожа с тем, как объекты передаются из метода в метод: большие и сложные структуры лучше передавать по ссылке, а вот простые типы будет быстрее скопировать.
Для получения копии из контейнера Cell с помощью метода get внутренний тип должен реализовывать трейт Copy.
Второй вариант: тип может реализовывать трейт Default. Тогда вы можете вызвать метод
take
для получения дефолтного объекта.Таким образом, в прошлом примере, где я использовал RefCell для изменения флага boolean, лучше подошёл бы Cell.
2️⃣
OnceCell<T>
В Rust версии 1.70 добавился новый контейнер –
std::cell::OnceCell
, который работает примерно так же, как Cell или RefCell, но позволяет установить значение внутри контейнера только один раз.Это можно сделать либо с помощью метода set (самостоятельно проконтролировав, что метод будет вызван только один раз), либо с помощью метода get_or_init, в который передаётся функция инициализации (она будет вызвана только при первом обращении к контейнеру).
Примеры можно посмотреть в официальной документации.
Ключевым преимуществом OnceCell является то, что он возвращает ссылку на внутренний объект (как RefCell), но не выполняет runtime проверок правил заимствования.
Реализация
OnceCell
не является потокобезопасной. В многопоточной среде нужно использовать соответствующий аналог: std::sync::OnceLock
.3️⃣
LazyCell<T, Func>
Контейнер
std::cell::LazyCell
был добавлен в Rust версии 1.80.LazyCell схож с OnceCell по смыслу и назначению, но позволяет указать функцию инициализации один раз при создании контейнера, вместо того, чтобы указывать её каждый раз в методe get_or_init, как для OnceCell. При этом данная функция тоже будет вызвана один раз при первом обращении к контейнеру для получения ссылки на объект.
Реализация
LazyCell
не является потокобезопасной. В многопоточной среде нужно использовать соответствующий аналог: std::sync::LazyLock
.С появлением контейнеров OnceCell и LazyCell в стандартной библиотеке языка необходимость в таких библиотеках, как lazy_static или once_cell, практически отпала.
Эти библиотеки я упоминал в посте про статическую память, там же есть пример их использования, который теперь можно реализовать только с помощью OnceCel/LazyCell.
#rust #rustlang #RustMemoryManagement
🔤 Строки в Rust: Часть 1. String vs str.
Новички, знакомящиеся с языком Rust, порой недоумевают, видя такие строчки кода:
или подобные ошибки компиляции:
⁉️ Зачем строку, объявленную в кавычках, нужно дополнительно приводить к типу String и почему её нельзя передавать в метод, ожидающий на входе String?
В других языках обычно с такими проблемами никто не сталкивается.
Всё дело в том, что в Rust существует сразу несколько типов для работы со строками.
Самые популярные и часто используемые из них – это
str
Во многих языках программирования есть такое понятие, как литеральные строки, то есть строки, объявленные в коде в виде текста в кавычках:
В Rust при компиляции литеральные строки попадают as is в бинарный файл, и программа может получить к ним доступ по определённому адресу.
Эту область памяти называют сегмент данных (data segment) и по своей сути она схожа со статическим сегментом памяти: данные доступны при старте программы и остаются неизменными на протяжении всей жизни программы.
Раз мы не можем каким-то образом изменить эти строки или повялить на их время жизни, Rust не даёт нам работать с типом
Таким образом, инициализируя переменную в коде литеральной строкой, мы получаем у этой переменной тип
String
Если же мы каким-то образом получаем строку в процессе работы программы, например вычитывая данные из файла, мы можем сохранить их в переменную типа
В отличие от
Наличие вектора подсказывает нам ещё несколько отличий от
❗️ Кстати, изменяемость строк – ещё одна необычная особенность для разработчиков, привыкшим к неизменным строкам в таких языках, как Java, JavaScript, Python и т.д.
Последнее отличие –
Как я уже отмечал выше, мы не можем владеть данными типа
❓ Что будет, если мы возьмём ссылку на
Тут есть несколько вариантов:
1️⃣ Мы можем получить данные типа
2️⃣ Мы можем получить слайс (срез) массива символов. Подробнее про слайсы можно почитать в документации.
Если коротко, то слайс – это ссылка на массив данных определённой длины. Мы можем получить срез всего массива или какой-то его части (подстроку). При этом слайс, в отличии String, не может быть изменён.
А какой тип у нас обладает такими же характеристиками: неизменяемая ссылка на массив UTF-8 символов?
Правильно,
Таким образом, типы переменных s2 и s3 из примера выше –
А литеральные строки типа
Продолжение ⬇️
#rust #rustlang #RustStringTypes
Новички, знакомящиеся с языком Rust, порой недоумевают, видя такие строчки кода:
let s: String = "строка".to_string();
или подобные ошибки компиляции:
fn main() {
log("Program has been started");
}
fn log(message: String) {
println!("{message}");
}
// mismatched types: expected `String`, found `&str`
⁉️ Зачем строку, объявленную в кавычках, нужно дополнительно приводить к типу String и почему её нельзя передавать в метод, ожидающий на входе String?
В других языках обычно с такими проблемами никто не сталкивается.
Всё дело в том, что в Rust существует сразу несколько типов для работы со строками.
Самые популярные и часто используемые из них – это
String
и str
, которые сегодня мы рассмотрим чуть более подробно.str
Во многих языках программирования есть такое понятие, как литеральные строки, то есть строки, объявленные в коде в виде текста в кавычках:
let s = "это литеральная строка";
В Rust при компиляции литеральные строки попадают as is в бинарный файл, и программа может получить к ним доступ по определённому адресу.
Эту область памяти называют сегмент данных (data segment) и по своей сути она схожа со статическим сегментом памяти: данные доступны при старте программы и остаются неизменными на протяжении всей жизни программы.
Раз мы не можем каким-то образом изменить эти строки или повялить на их время жизни, Rust не даёт нам работать с типом
str
напрямую, а предоставляет только immutable ссылку – &str
.Таким образом, инициализируя переменную в коде литеральной строкой, мы получаем у этой переменной тип
&str
.
let s: &str = "это литеральная строка";
String
Если же мы каким-то образом получаем строку в процессе работы программы, например вычитывая данные из файла, мы можем сохранить их в переменную типа
String
.В отличие от
str
, String
не является примитивным типом, а представляет собой структуру-обёртку над динамическим массивом, где элементы массива – это символы строки в кодировке UTF-8:
pub struct String {
vec: Vec<u8>,
}
Наличие вектора подсказывает нам ещё несколько отличий от
str
: String
сохраняется в Heap, её длина не фиксированна и такие строки могут изменяться в процессе работы программы.❗️ Кстати, изменяемость строк – ещё одна необычная особенность для разработчиков, привыкшим к неизменным строкам в таких языках, как Java, JavaScript, Python и т.д.
Последнее отличие –
String
является так называемым owned типом данных, то есть типом, которым может владеть объявленная нами переменная. Как я уже отмечал выше, мы не можем владеть данными типа
str
, поэтому &str
– заимствованный (borrowed) тип данных.❓ Что будет, если мы возьмём ссылку на
String
?Тут есть несколько вариантов:
1️⃣ Мы можем получить данные типа
&String
, т.е. ссылку на структуру String в Heap, которая внутри хранит в себе ссылку на массив символов, расположенных тоже где-то в Heap.
let s = String::from("abcd");
let s1: &String = &s;
2️⃣ Мы можем получить слайс (срез) массива символов. Подробнее про слайсы можно почитать в документации.
Если коротко, то слайс – это ссылка на массив данных определённой длины. Мы можем получить срез всего массива или какой-то его части (подстроку). При этом слайс, в отличии String, не может быть изменён.
let s = String::from("abcd");
let s2 = &s[0..s.len()]; // вся строка
let s3 = &s[0..2]; // первые два символаы
А какой тип у нас обладает такими же характеристиками: неизменяемая ссылка на массив UTF-8 символов?
Правильно,
&str
!Таким образом, типы переменных s2 и s3 из примера выше –
&str
.А литеральные строки типа
&str
– это ничто иное, как слайс массива символов.Продолжение ⬇️
#rust #rustlang #RustStringTypes
🔤 Строки в Rust: Часть 2. Преобразование String в &str и обратно.
Теперь, когда мы разобрались со
Глядя на то, как с помощью оператора
Но всё гораздо проще: в Rust для типа String реализован трейт Deref в тип str.
А значит мы получаем такое преобразование с помощью неявного приведения типов:
💡 Отсюда следует хорошая практика:
в функциях, которые работают со ссылками на строки, лучше объявлять переменные с типом
Если заменить тип переменной message в функции log на
Если с преобразованием
1️⃣ to_owned()
Для типа
Преобразование происходит путём копирования данных с помощью всяких низкоуровневых функций.
Обычно используется тогда, когда разработчик хочет подчеркнуть именно эту особенность конвертации: получение владения над строкой.
2️⃣ String::from()
Один из самых популярных способов – вызов метода from (благодаря реализации трейта From).
Внутри вызывается метод
Обычно используется тогда, когда хотят подчеркнуть создание типа
При конвертации литеральных строк мне этот вариант не сильно нравится, так как длинный префикс немного отвлекает от самих данных, которые могут быть важнее, чем их тип.
3️⃣ to_string()
Ещё один популярный способ – вызов метода трейта
Внутри вызывается
Я чаще всего использую именно этот способ, так как он и отображает моё намерение создать переменную типа
4️⃣ into()
Раз для String реализован трейт
С точки зрения реализации тут та же самая "матрёшка", что и для метода
Этот вариант может быть самым коротким, особенно если дальше переменная в явном виде используется как String, и Rust сможет вывести её тип самостоятельно без указания его при объявлении.
Но мне такой подход не очень нравится, так как теряется информация о типе.
Этот вариант я люблю использовать, когда передаю
❗️ Как видите, какой бы вариант преобразования вы ни выбрали, всё в итоге сводится к вызову метода
А значит выбор подхода целиком зависит от того, какие намерения вы хотите заложить в текст своей программы.
#rust #rustlang #RustStringTypes
Теперь, когда мы разобрались со
String
, &String
, str
и &str
, посмотрим каким образом происходит преобразование этих типов друг в друга.Глядя на то, как с помощью оператора
&
мы конвертируем тип String
в другой тип &str
, вы могли подумать, что для такой операции используется какая-то внутренняя магия компилятора.Но всё гораздо проще: в Rust для типа String реализован трейт Deref в тип str.
А значит мы получаем такое преобразование с помощью неявного приведения типов:
let s1 = String::from("abcd");
let s2: &str = &s1;
💡 Отсюда следует хорошая практика:
в функциях, которые работают со ссылками на строки, лучше объявлять переменные с типом
&str
, а не &String
, так как это позволяет одинаково удобно передавать в функцию обычные строки, строковые слайсы и литеральные строки:
fn log(message: &str) {
println!("{message}");
}
fn main() {
let message = String::from("message");
// вся строка
log(&message);
// подстрока
log(&message[0..4]);
// литеральная строка
log("another message");
}
Если заменить тип переменной message в функции log на
&String
, то 2й и 3й вариант вызова функции станет некорректным.Если с преобразованием
String
в &str
всё более или менее однозначно, то вот для конвертации &str
в String
существует целая россыпь способов:1️⃣ to_owned()
let s = "abcd".to_owned();
Для типа
&str
реализован метод to_owned()
, который позволяет из borrowed типа &str
получить owned тип String
.Преобразование происходит путём копирования данных с помощью всяких низкоуровневых функций.
Обычно используется тогда, когда разработчик хочет подчеркнуть именно эту особенность конвертации: получение владения над строкой.
2️⃣ String::from()
let s = String::from("abcd");
Один из самых популярных способов – вызов метода from (благодаря реализации трейта From).
Внутри вызывается метод
to_owned()
.Обычно используется тогда, когда хотят подчеркнуть создание типа
String
.При конвертации литеральных строк мне этот вариант не сильно нравится, так как длинный префикс немного отвлекает от самих данных, которые могут быть важнее, чем их тип.
3️⃣ to_string()
let s = "abcd".to_string();
Ещё один популярный способ – вызов метода трейта
ToString
.Внутри вызывается
String::from()
, который в свою очередь вызывает внутри to_owned()
.Я чаще всего использую именно этот способ, так как он и отображает моё намерение создать переменную типа
String
и не отвлекает от данных.4️⃣ into()
let s: String = "abcd".into();
Раз для String реализован трейт
From<&str>
, значит и для &str реализован трейт Into<String>
.С точки зрения реализации тут та же самая "матрёшка", что и для метода
to_string()
: внутри вызывается from()
, который в свою очередь вызывает to_owned()
.Этот вариант может быть самым коротким, особенно если дальше переменная в явном виде используется как String, и Rust сможет вывести её тип самостоятельно без указания его при объявлении.
Но мне такой подход не очень нравится, так как теряется информация о типе.
Этот вариант я люблю использовать, когда передаю
&str
в какой-то метод и хочу показать, что мне не важно какой там дальше тип – String или какой-то другой, для которого реализован трейт Into.
fn save_data(data: String) {}
fn main() {
save_data("some data".into());
}
❗️ Как видите, какой бы вариант преобразования вы ни выбрали, всё в итоге сводится к вызову метода
to_owned()
.А значит выбор подхода целиком зависит от того, какие намерения вы хотите заложить в текст своей программы.
#rust #rustlang #RustStringTypes
❤2👍1🔥1