Паттерн typestate: пример
Паттерн typestate кодирует часть состояния значения во время выполнения в его тип. Это позволяет предотвращать недопустимые или неприменимые операции на этапе компиляции.
// Copyright 2025 Google LLC
// SPDX-License-Identifier: Apache-2.0
use std::fmt::Write as _;
#[derive(Default)]
struct Serializer {
output: String,
}
struct SerializeStruct {
serializer: Serializer,
}
impl Serializer {
fn serialize_struct(mut self, name: &str) -> SerializeStruct {
writeln!(&mut self.output, "{name} {{").unwrap();
SerializeStruct { serializer: self }
}
fn finish(self) -> String {
self.output
}
}
impl SerializeStruct {
fn serialize_field(mut self, key: &str, value: &str) -> Self {
writeln!(&mut self.serializer.output, " {key}={value};").unwrap();
self
}
fn finish_struct(mut self) -> Serializer {
self.serializer.output.push_str("}\n");
self.serializer
}
}
fn main() {
let serializer = Serializer::default()
.serialize_struct("User")
.serialize_field("id", "42")
.serialize_field("name", "Алиса")
.finish_struct();
println!("{}", serializer.finish());
}
Схема использования Serializer:
-
Этот пример вдохновлён трейтом
Serializerиз Serde. Serde использует typestate внутри себя, чтобы сериализация следовала допустимой структуре. Подробнее см.: https://serde.rs/impl-serializer.html -
Ключевая идея typestate в том, что переходы между состояниями происходят путём потребления одного значения и создания нового. На каждом шаге доступны только операции, допустимые для этого состояния.
-
В этом примере:
-
Начинаем с
Serializer, который позволяет только начать сериализацию структуры. -
После вызова
.serialize_struct(...)владение переходит к значениюSerializeStruct. С этого момента мы можем вызывать только методы, связанные с сериализацией полей структуры. -
Исходный
Serializerбольше недоступен — это не позволяет смешивать режимы (например, начать другую структуру внутри структуры) или вызватьfinish()слишком рано. -
Только после вызова
.finish_struct()мы снова получаемSerializer. На этом этапе вывод можно финализировать или переиспользовать.
-
-
Если мы забудем вызвать
finish_struct()и отбросимSerializeStructраньше времени,Serializerтоже будет освобождён. Это гарантирует, что неполный вывод не попадёт в систему. -
Для сравнения: если бы мы реализовали всё непосредственно в
Serializer, как показано на предыдущем слайде, ничто не помешало бы пропустить важные шаги или смешать потоки сериализации.