При проектировании конечных автоматов в Rust хранение информации о текущем состоянии системы очень часто производится в объекте типа соответствующего его состоянию. При этом изменение состояния системы вызывает создание экземпляра другого типа (соответствующего её состоянию).

Выбор такого подхода в Rust связан со следующими особенностями:

  • Rust использует концепцию анализа Typestate

  • Используется строгая типизация данных, нет средств для автоматического создания и хранения объектов без инициализации всех значений для описанных полей данных. Для инициализации “сложных” объектов часто применяется шаблон проектирования Строитель (Builder)

  • Rust не предоставляет средств для автоматической трансформации типов. Для перевода экземпляра данных одного типа в другой используется вызов соответствующих методов типа (например into_f32())

  • Язык Rust, избегая состояния гонки данных, повсеместно использует понятия владения и заимствования. Для перевода экземпляра данных одного типа в другой данные либо меняют владельца (и не доступны в первичном экземпляре после этого), либо в памяти должна быть размещена копия этих данных. Заимствование значения из исходного экземпляра в данном случае создаёт больше проблем.


Простейший пример построения конечного автомата имеющего два состояния:

  1. состояние с не подготовленными данными будем хранить в типе FooInit (реализует шаблон проектирования Строитель);

  2. состояние с данными готовыми к использованию будем хранить в типе FooReady.

Реализация шаблона с однократным использованием структуры данных FooInit (созданием нового экземпляра на каждой итерации подготовки данных):

pub mod foo_system{
  // Структура данных для работы с готовыми данными
  #[derive(Debug)]
  pub struct FooReady {
    c: u32,
  }

  // Структура данных для подготовки данных по шаблону Строитель
  pub struct FooInit {
    a: u32,
    b: u32,
  }

  // Реализация методов для шаблона Строитель
  impl FooInit {
    // Конструктор объекта FooInit с данными по умолчанию
    pub fn new() -> Self {
      Self {
        a: 0,
        b: 0,
      }
    }

    // Подготовка данных в поле 'a' FooInit
    pub fn set_a(self, a: u32) -> Self {
      // Создаём новую структуру данных
      Self {
        a,
        ..self // заполняем другие поля из исходного экземпляра
      }
    }

    // Подготовка данных в поле 'b' FooInit
    pub fn set_b(self, b: u32) -> Self {
      // Создаём новую структуру данных
      Self {
        b,
        ..self // заполняем другие поля из исходного экземпляра
      }
    }

    // Смена состояния FooInit -> FooReady
    pub fn into_foo(self) -> FooReady {
      FooReady {
        c: self.a + self.b,
      }
    }
  }
}

fn main() {
  // Создаём конечный автомат в состоянии FooInit
  let foo = foo_system::FooInit::new()
  
  // Подготавливаем данные в поле 'a'
  .set_a(1)

  // Подготавливаем данные в поле 'b'
  .set_b(2)

  // Переводим систему в состояние FooReady
  .into_foo();

  // Работаем с системой в состоянии FooReady
  println!("{:#?}", foo);
}

Реализация шаблона с повторным использованием структуры данных FooInit (изменением данных в исходной структуре на каждой итерации подготовки данных):

pub mod foo_system{
  // Структура данных для работы с готовыми данными
  #[derive(Debug)]
  pub struct FooReady {
    c: u32,
  }

  // Структура данных для подготовки данных по шаблону Строитель
  pub struct FooInit {
    a: u32,
    b: u32,
  }

  // Реализация методов для шаблона Строитель
  impl FooInit {
    // Конструктор объекта FooInit с данными по умолчанию
    pub fn new() -> Self {
      Self {
        a: 0,
        b: 0,
      }
    }

    // Подготовка данных в поле 'a' FooInit
    pub fn set_a(&mut self, a: u32) -> &Self {
      self.a = a; // меняем данные в структуре
      self
    }

    // Подготовка данных в поле 'b' FooInit
    pub fn set_b(&mut self, b: u32) -> &Self {
      self.b = b; // меняем данные в структуре
      self
    }

    // Смена состояния FooInit -> FooReady
    pub fn into_foo(&self) -> FooReady {
      FooReady {
        c: self.a + self.b,
      }
    }
  }
}

fn main() {
  // Создаём конечный автомат в состоянии FooInit
  let mut foo = foo_system::FooInit::new();
  
  // Подготавливаем данные в поле 'a'
  foo.set_a(1);

  // Подготавливаем данные в поле 'b'
  foo.set_b(2);

  // Переводим систему в состояние FooReady
  let foo = foo.into_foo();

  // Работаем с системой в состоянии FooReady
  println!("{:#?}", foo);
}

В обоих вариантах реализации результатом исполнения будет вывод в консоль:

FooReady {
  c: 3,
}