В некоторых языках есть "необязательные аргументы" в функциях или перегрузка методов. В Rust этого нет. Если структура состоит из 10 полей, и большинство из них имеют стандартные значения, создавать её напрямую через Struct { ... } очень больно — придется прописывать каждое поле.
Для этого используется паттерн Builder. Мы создаем отдельную структуру, которая пошагово собирает наш объект.
Представь, что мы настраиваем веб-сервер.
// 1. Финальная структура сервера (которую мы хотим получить)
#[derive(Debug)]
struct Server {
host: String,
port: u16,
tls_enabled: bool,
}
// 2. Структура-строитель
struct ServerBuilder {
host: String,
port: u16,
tls_enabled: bool,
}
impl ServerBuilder {
// Начальное состояние
fn new(host: &str) -> Self {
ServerBuilder {
host: host.to_string(),
port: 80, // Стандартный порт
tls_enabled: false, // Без шифрования по умолчанию
}
}
// Методы-модификаторы.
// Обрати внимание: они принимают `mut self` (забирают владение строителем),
// меняют его и возвращают обратно `Self`. Это позволяет выстраивать цепочку!
fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
fn tls(mut self, enabled: bool) -> Self {
self.tls_enabled = enabled;
self
}
// Финальный метод, который превращает Строителя в готовый Сервер
fn build(self) -> Server {
Server {
host: self.host,
port: self.port,
tls_enabled: self.tls_enabled,
}
}
}
fn main() {
// Магия! Мы собираем сервер цепочкой вызовов (method chaining).
let my_server = ServerBuilder::new("127.0.0.1")
.port(8080)
.tls(true)
.build();
println!("Сервер запущен: {:#?}", my_server);
}Этот подход невероятно популярен в экосистеме Rust. Если ты будешь использовать любую крупную библиотеку (например, для создания HTTP-запросов), ты столкнешься с Builder'ом.
Это самая уникальная и мощная фишка Rust.
В классическом программировании, если у тебя есть документ, он может быть "Черновиком", "На проверке" и "Опубликованным". Обычно это делают так: создают одно поле state (состояние) внутри структуры. И в каждом методе (например, publish()) пишут if state == "Черновик" { ОШИБКА }.
Проблема в том, что эта ошибка всплывет только во время работы программы (в рантайме).
В Rust мы используем систему типов и Владение, чтобы сделать неверные действия физически невозможными на этапе компиляции! Суть в том, что каждое состояние — это отдельная структура.
Давай создадим систему публикации постов:
// 1. Создаем три разные структуры для каждого состояния
struct DraftPost {
content: String,
}
struct PendingReviewPost {
content: String,
}
struct PublishedPost {
content: String,
}
// 2. Метод создания всегда возвращает Черновик
impl DraftPost {
fn new(text: &str) -> DraftPost {
DraftPost { content: text.to_string() }
}
// МАГИЯ ЗДЕСЬ: Метод забирает владение `self` (Черновиком)
// и возвращает НОВЫЙ тип (Пост на проверке).
// Старый черновик уничтожается!
fn request_review(self) -> PendingReviewPost {
PendingReviewPost { content: self.content }
}
}
impl PendingReviewPost {
// Одобряем пост. Забираем Пост на проверке, выдаем Опубликованный.
fn approve(self) -> PublishedPost {
PublishedPost { content: self.content }
}
// Если отклоняем - возвращаем обратно Черновик
fn reject(self) -> DraftPost {
DraftPost { content: self.content }
}
}
impl PublishedPost {
// И только Опубликованный пост имеет метод "прочитать"
fn read(&self) -> &str {
&self.content
}
}
fn main() {
let my_draft = DraftPost::new("Изучаем Typestate");
// my_draft.read();
// ОШИБКА КОМПИЛЯЦИИ! У Черновика нет метода read().
// Мы физически не можем прочитать неопубликованный пост!
let review_post = my_draft.request_review();
// my_draft больше не существует! Мы передали Владение (move).
// Мы не можем случайно запросить ревью дважды.
let published = review_post.approve();
// А вот теперь можно читать
println!("Пост в эфире: {}", published.read());
}Почему это так круто? Компилятор Rust выступает гарантом нашей бизнес-логики. Мы не можем "выстрелить из незаряженного ружья", потому что у типа UnloadedGun просто нет метода fire(). Метод fire() есть только у типа LoadedGun. Это паттерн делает код железобетонным.
В базах данных идентификаторы пользователя, заказа и товара — это часто просто большие числа (например, i64).
Представь такую функцию:
fn pay_for_order(user_id: i64, order_id: i64) {
// логика оплаты
}Если программист случайно вызовет её так: pay_for_order(order_id, user_id), компилятор промолчит (ведь это просто два числа i64). Программа спишет деньги не с того человека!
Чтобы избежать этой катастрофы, в Rust используется паттерн Newtype. Мы оборачиваем базовый тип в "структуру-кортеж с одним элементом".
// Создаем новые уникальные типы
struct UserId(i64);
struct OrderId(i64);
// Теперь компилятор строго следит за тем, что мы передаем!
fn secure_pay(user: UserId, order: OrderId) {
println!("Оплата заказа {} пользователем {}", order.0, user.0); // Доступ через .0
}
fn main() {
let uid = UserId(100500);
let oid = OrderId(999);
// secure_pay(oid, uid);
// ОШИБКА КОМПИЛЯЦИИ! Компилятор скажет: "Ожидался UserId, а получен OrderId".
secure_pay(uid, oid); // Работает идеально
}Бонус производительности: Точно так же, как и с дженериками, эта обертка существует только на этапе компиляции. Когда программа будет собрана, в памяти это будут просто "голые" числа i64. Защита абсолютно бесплатна.
Резюме:
- Builder помогает пошагово и элегантно создавать структуры с кучей параметров.
- Typestate использует систему типов и правила Владения, чтобы сделать неверную бизнес-логику невозможной для компиляции. Состояние — это тип. Смена состояния — это уничтожение старого типа и возврат нового.
- Newtype оборачивает простые типы (числа, строки) в уникальные структуры, чтобы компилятор бил по рукам, если мы перепутаем ID пользователя с ID заказа.