Привет, Хабр!
Иммутабельность данных в Rust – это основа для создания систем, устойчивых к ошибкам и сайд-эффектам. В этой статье рассмотрим, как Rust позволяет использовать неизменяемые структуры данных для улучшения производительности и безопасности приложений.
Начнем с синтаксических особенностей.
Синтаксические особенности
В Rust переменные по умолчанию иммутабельны. То есть после их инициализации изменить значение нельзя. Это основной аспект языка, который помогает предотвратить множество видов ошибок, связанных с состоянием данных. Для объявления переменной используется ключевое слово let
:
let x = 5;
// x = 6; // это вызовет ошибку компиляции, так как x неизменяемая
Если нужно изменить значение переменной, можно использовать модификатор mut
, который явно указывает, что переменная может быть изменена:
let mut y = 5;
y = 6; // теперь это корректный код
Ссылки в Rust также иммутабельны по умолчанию. То есть нельзя изменить данные, на которые ссылаетесь, без явного указания:
let z = 10;
let r = &z;
// *r = 11; // ошибка, так как r — иммутабельная ссылка
Для изменения данных через ссылку нужно использовать изменяемую ссылку:
let mut a = 10;
let b = &mut a;
*b = 11; // корректно, так как b — изменяемая ссылка
Структуры данных в Rust также подчиняются правилам иммутабельности. Если создается экземпляр структуры с помощью let
, все его поля будут неизменяемыми, если только каждое поле явно не объявлено как mut
:
struct Point {
x: i32,
y: i32,
}
let point = Point { x: 0, y: 0 };
// point.x = 5; // ошибка, так как поля структуры неизменяемы
Помимо всего этого, есть ряд функциональных возможностей, которые способствуют работе с иммутабельными структурами данных. Одна из таких возможностей — это шаблон Создатель, позволяющий изменять данные структуры в процессе её создания, но предоставляя в результате неизменяемый объект:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn new() -> Rectangle {
Rectangle { width: 0, height: 0 }
}
fn set_width(&mut self, width: u32) -> &mut Rectangle {
self.width = width;
self
}
fn set_height(&mut self, height: u32) -> &mut Rectangle {
self.height = height;
self
}
fn build(self) -> Rectangle {
self
}
}
let rect = Rectangle::new().set_width(10).set_height(20).build();
// rect.width = 15; // ошибка, так как rect неизменяемый после создания
Здесь Rectangle
создается как изменяемый для настройки его размеров, но после вызова метода build
он становится неизменяемым.
Типичные иммутабельные структуры
Иммутабельные векторы
В Rust векторы по умолчанию являются изменяемыми, но можно использовать библиотеку, такую как im
, которая предоставляет иммутабельные коллекции. Пример создания и использования иммутабельного вектора:
use im::vector::Vector;
fn main() {
let vec = Vector::new();
let updated_vec = vec.push_back(42);
println!("Original vector: {:?}", vec);
println!("Updated vector: {:?}", updated_vec);
}
Здесь updated_vec
является новым вектором, содержащим добавленные элементы, в то время как оригинальный вектор vec
остается неизменным.
Структурный общий доступ
Структурный общий доступ позволяет иммутабельным структурам данных делиться частями своего состояния с другими структурами, минимизируя тем самым необходимость копирования данных. Пример можно реализовать с помощью библиотеки rpds
, которая имеет персистентные структуры данных:
use rpds::Vector;
fn main() {
let vec = Vector::new().push_back(10).push_back(20);
let vec2 = vec.push_back(30);
println!("vec2 shares structure with vec: {:?}", vec2);
}
vec2
использует большую часть данных из vec
, добавляя только новые элементы.
Иммутабельные связные списки
Иммутабельные связные списки полезны в функциональном программировании. Пример использования персистентного связного списка:
use im::conslist::ConsList;
fn main() {
let list = ConsList::new();
let list = list.cons(1).cons(2).cons(3);
println!("Persistent list: {:?}", list);
}
Каждая операция cons
создает новый список, который содержит новый элемент наряду со ссылкой на предыдущий список.
Иммутабельные хэш-карты
Иммутабельные хэш-карты могут использоваться для хранения и доступа к данным по ключу:
use im::HashMap;
fn main() {
let mut map = HashMap::new();
map = map.update("key1", "value1");
let map2 = map.update("key2", "value2");
println!("Map1: {:?}", map);
println!("Map2: {:?}", map2);
}
Здесь map2
добавляет новую пару ключ-значение, при этом map
остается неизменной.
Иммутабельные деревья
Иммутабельные деревья можно использовать для создания сложных структур данных с операциями поиска и вставки:
use im::OrdMap;
fn main() {
let tree = OrdMap::new();
let tree = tree.update(1, "a").update(2, "b");
let tree2 = tree.update(3, "c");
println!("Tree1: {:?}", tree);
println!("Tree2: {:?}", tree2);
}
Примеры использования
Многопоточный доступ к конфигурации
Разработаем примеры системы, где множество потоков должны получать доступ к общей конфигурации без риска гонок данных. Иммутабельность здесь полезна тем, что гарантирует, что данные не будут случайно изменены, что, как мы знаем, очень важно в многопоточном окружении.
Определим иммутабельную структуру AppConfig
, содержащую конфигурационные параметры:
#[derive(Clone, Debug)]
struct UserState {
user_id: u32,
preferences: Vec<String>,
}
Создадим глобально доступный Arc
для этой конфигурации, чтобы безопасно делиться между потоками:
impl UserState {
fn add_preference(&self, preference: String) -> Self {
let mut new_preferences = self.preferences.clone();
new_preferences.push(preference);
UserState {
user_id: self.user_id,
preferences: new_preferences,
}
}
}
Здесь каждый поток получает безопасный доступ к конфигурации, что исключает возможность её изменения, т.к данные защищены иммутабельностью и Arc
.
Управление состоянием в функциональном веб-приложении
Второй кейс — это веб-приложение, где состояние пользователя обновляется без мутаций, используя концепции ФП для улучшения управляемости состояния и упрощения тестирования.
Определим иммутабельную структуру состояния пользователя:
#[derive(Clone, Debug)]
struct UserState {
user_id: u32,
preferences: Vec<String>,
}
Функция обновления состояния, возвращающая новое состояние:
impl UserState {
fn add_preference(&self, preference: String) -> Self {
let mut new_preferences = self.preferences.clone();
new_preferences.push(preference);
UserState {
user_id: self.user_id,
preferences: new_preferences,
}
}
}
Пример в контексте обработки запроса:
fn handle_request(current_state: &UserState) -> UserState {
let updated_state = current_state.add_preference("new_preference".to_string());
updated_state
}
Здесь каждый вызов add_preference
создаёт новую версию состояния UserState
.
Полезные библиотеки
im
— это высокопроизводительная библиотека для работы с иммутабельными структурами данных в Rust. Она имеет полный набор персистентных структур данных: списки, векторы, карты и множества, которые сохраняют предыдущие версии себя при модификациях и позволяют разделять данные между состояниями без необходимости их полного копирования.
Пример иммутабельного списка:
use im::ConsList;
fn main() {
let list = ConsList::new();
let list = list.cons(1).cons(2).cons(3);
println!("Persistent list: {:?}", list);
}
Создаем список с помощью метода cons
, который добавляет элемент в начало списка, сохраняя при этом неизменной предыдущую версию списка. Это суперски подходит для функциональных программ, где неизменяемость данных важна.
rpds
, которую мы применяли чуть выше, предоставляет коллекцию иммутабельных и персистентных структур данных. Библиотека поддерживает функциональный стиль, предлагая структуры, которые автоматом сохраняют историю изменений.
Пример использования иммутабельного словаря:
use rpds::HashTrieMap;
fn main() {
let map = HashTrieMap::new();
let map = map.insert("key1", "value1");
let map2 = map.insert("key2", "value2");
println!("Map1: {:?}", map);
println!("Map2: {:?}", map2);
}
Здесь map2
создается на основе map
с добавлением новой пары ключ-значение, при этом оригинальный map
остается неизменным.
Благодаря иммутабельности в Rust, можно управлять состоянием приложений, избегая сложностей, связанных с мутабельными структурами данных.
В завершение хочу пригласить вас на бесплатный вебинар, где мы подробно рассмотрим различия и особенности разработки на Rust для классического backend и для блокчейн-систем. Регистрация доступна по ссылке.
Комментарии (9)
ZyXI
08.08.2024 18:21+5Явно видно непонимание иммутабельности. К примеру,
let rect = Rectangle::new().set_width(10).set_height(20).build(); // rect.width = 15; // ошибка, так как rect неизменяемый после создания
Во‐первых, код не работает. Вы не можете скормить
.build()
&mut self
вместоself
, а именно первое возвращает.set_height
.
Во‐вторых,build
полностью бесполезна. Иммутабельность относится к конкретному месту размещения, если вы владеете объектом, то вы всегда можете сделать его мутабельным. Реальная иммутабельность достигается инкапсуляцией и удалением из общего доступа методов, которые могут изменить состояние объектов. Соответственно в шаблоне создатель у вас должен был быть тип RectangleBuilder и тип Rectangle, и только первый должен иметь методыset_width
.
Соответственно, говорить «создадим иммутабельную структуру», не объясняя нигде, что она иммутабельная только пока в общем доступе нет методов, её изменяющих, некорректно. Напомню, что вот это вполне себе корректный код:let mut rect = Rectangle::new(); rect.set_width(10); let rect = rect.build(); let rect = rect; // То же, что и строчка выше. // rect.set_width(20); // Не скомпилируется. let mut rect = rect; rect.set_width(30); // А теперь мы опять можем менять rect.
Остальная часть не лучше. Например:
Если создается экземпляр структуры с помощью let, все его поля будут неизменяемыми, если только каждое поле явно не объявлено как mut:
Можете показать, как вы объявляете поля структуры как
mut
? Только так, чтобы результат компилировался.Или «создадим глобально доступный
Arc
»: «код, в котором Arc нигде не упоминается».Иммутабельность в Rust в нескольких предложениях:
Если вы не объявили что‐то, как
mut
, то взятие исключающей (&mut
) ссылки вам недоступно, как и всё, что требует её взятия.Изменение значения требует такой ссылки (или возможности её взятия) или внутренней мутабельности в каком‐то виде.
Исключающая ссылка гарантирует только, что объект больше никому не доступен. Разделяемая ссылка (
&
) гарантирует только, что на объект нет исключающих ссылок. Иммутабельность здесь вообще не причём, ссылки исторически назвали неправильно, но большинство типов компилятор позволит менять только по исключающей ссылке.
Cheater
08.08.2024 18:21+4// А теперь мы опять можем менять rect.
В смысле "можем менять rect"?? Вы же в курсе, что
let mut rect = rect
создаёт новый объект и мувает в него старый, а не снимает с объекта константность? Выведите на печать&rect
до и после выражения с let, они будут разными.ZyXI
08.08.2024 18:21Это с точки зрения семантики языка. С точки зрения программиста это часто один и тот же объект, т.к. нет видимой разницы (разные адреса, если оптимизатор почему‐то не смог убрать перемещение, не считаются). Это особенно верно, если под
rect
у нас не простая структура данных, а что‐то вродеBox<Rectangle>
: кого волнует, где в стеке указатель наRectangle
, если данные всё равно за указателем и никуда не двигаются?
ZyXI
08.08.2024 18:21После некоторого размышления у меня появилось сильное подозрение, что семантика
let foo = bar;
— это «создать новое местоfoo
и переместить в него объект изbar
» (объект один и тот же, но его переместили), а не «создать новый объектfoo
и переместить в него данные изbar
,bar
удалить» (есть два разных объекта с непересекающимися временами жизни).
Я не могу найти конкретных подтверждений в документации (но то, как про перемещенияобъектовзначений рассказывает документацияstd::pin
оставляет ощущение, что я скорее прав), ноlet foo = foo
точно не запускаетDrop::drop
и при этом никаких конструкторов в Rust нет. Можете обосновать утверждение «let mut rect = rect
создаёт новый объект»?
vlad4kr7
08.08.2024 18:21let rect = Rectangle{width: 10, height: 20}; // короче и понятнее let rect = Rectangle{width: 20, ..rect}; // не мутировали, но в тему статье
bromzh
Жесть, на курсах такой же говнокод учите писать? Самое смешное, что вы даже не проверили код, он не рабочий:
Ну и до кучи, чтобы мутировать поля достаточно просто добавить mut, к чему эти пляски с недобилдером были?
redfox0
Тут скорее хотели показать приватные поля в структуре. Но надо было структуру переместить в другой файл/модуль (ключевое слово
mod
):bromzh
Если бы хотели - так бы и написали. Тут скорее хотели порекламировать свои курсы, но забыли, что раст они не знают. Как можно воспринимать это всерьёз, если они пишут вот такое: