Привет, Хабр!
Иммутабельность данных в 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 и для блокчейн-систем. Регистрация доступна по ссылке.