Что такое конечный автомат (State Machine)?

Ответ

Конечный автомат (State Machine, FSM) — это поведенческий паттерн, который позволяет объекту изменять своё поведение в зависимости от внутреннего состояния. Он формализует переходы между конечным числом состояний в ответ на события.

Основные компоненты:

  1. Состояния (States): Определённые конфигурации системы (например, Idle, Processing, Completed).
  2. Переходы (Transitions): Правила, определяющие, как и при каких событиях происходит смена состояния.
  3. События (Events): Входные сигналы или действия, инициирующие переходы.

Практический пример: Заказ в интернет-магазине

public enum OrderState { New, Paid, Shipped, Delivered, Cancelled }

public class Order
{
    public OrderState CurrentState { get; private set; } = OrderState.New;
    private Dictionary<(OrderState, string), OrderState> _transitions;

    public Order()
    {
        // Определяем правила переходов: (Текущее состояние, Событие) -> Новое состояние
        _transitions = new Dictionary<(OrderState, string), OrderState>
        {
            { (OrderState.New, "Pay"), OrderState.Paid },
            { (OrderState.Paid, "Ship"), OrderState.Shipped },
            { (OrderState.Shipped, "Deliver"), OrderState.Delivered },
            { (OrderState.New, "Cancel"), OrderState.Cancelled },
            { (OrderState.Paid, "Cancel"), OrderState.Cancelled }
        };
    }

    public bool TryProcessEvent(string eventName)
    {
        var key = (CurrentState, eventName);
        if (_transitions.TryGetValue(key, out var newState))
        {
            CurrentState = newState;
            Console.WriteLine($"Order state changed to {CurrentState} after event '{eventName}'");
            return true;
        }
        Console.WriteLine($"Invalid transition from {CurrentState} on event '{eventName}'");
        return false;
    }
}

// Использование
var order = new Order();
order.TryProcessEvent("Pay");     // OK: New -> Paid
order.TryProcessEvent("Ship");    // OK: Paid -> Shipped
order.TryProcessEvent("Pay");     // Ошибка: Нет перехода из Shipped по событию Pay

Зачем это нужно?

  • Упрощение сложной логики: Вместо множества if-else или switch по флагам используется чётко определённая таблица переходов.
  • Предсказуемость: Все возможные состояния и переходы явно описаны.
  • Лёгкость тестирования: Можно протестировать каждый переход независимо.
  • Соблюдение инвариантов: Невозможны недопустимые переходы (например, отменить уже доставленный заказ).

State Machine и async/await в .NET:

Компилятор C# преобразует асинхронные методы (async/await) в машину состояний (State Machine). Это структура, реализующая интерфейс IAsyncStateMachine, которая:

  • Хранит текущую позицию выполнения метода (в виде целого числа state).
  • «Поднимает» локальные переменные в поля структуры, чтобы их значения сохранялись между await.
  • Управляет продолжением выполнения после завершения асинхронной операции.

Почему так сделано? Это позволяет разбить метод на логические шаги, приостанавливать выполнение без блокировки потока и возобновлять его с нужного места, что и создаёт иллюзию последовательного асинхронного кода.

Ответ 18+ 🔞

Давай я тебе на пальцах объясню, что за зверь такой — конечный автомат, а то звучит страшно, как из курса кибернетики.

Представь себе кофемашину, обычную. У неё есть состояния, да? «Выключена», «Готова», «Варит кофе», «Требует чистку». И события: ты «нажал кнопку», «закончил цикл», «забыл промыть».

Так вот, если она в состоянии «Выключена», и ты «нажал кнопку» — нихуя не произойдёт. Она не начнёт варить. А вот если она в состоянии «Готова», и ты «нажал кнопку» — вот тогда она перейдёт в состояние «Варит кофе». Это и есть переход. Всё просто, ёпта.

В коде это выглядит как избавление от адского ада с if-else. Вместо того чтобы писать хуету типа:

if (state == "New" && event == "Pay") { ... }
else if (state == "Paid" && event == "Ship") { ... }
else if (state == "Shipped" && event == "Deliver") { ... }
else if (state == "New" && event == "Cancel") { ... }
// ... и так до посинения, блядь

Ты делаешь по-человечески. Таблицу переходов, чёткую, как слеза младенца:

_transitions = new Dictionary<(OrderState, string), OrderState>
{
    { (OrderState.New, "Pay"), OrderState.Paid },
    { (OrderState.Paid, "Ship"), OrderState.Shipped },
    { (OrderState.Shipped, "Deliver"), OrderState.Delivered },
    { (OrderState.New, "Cancel"), OrderState.Cancelled },
    { (OrderState.Paid, "Cancel"), OrderState.Cancelled }
    // А вот (OrderState.Shipped, "Cancel") — нету! Нельзя отменить уже отправленный заказ, логично же.
};

И потом просто тыкаешь: «эй, машина, вот событие, вали из текущего состояния». Если переход в таблице есть — ок. Нет — получи, дружок, ошибку, иди нахуй. Никаких неожиданностей.

А теперь самое интересное, про async/await. Ты же в C# пишешь async метод, там await какой-нибудь задачи, и всё такое. Так вот, компилятор смотрит на эту твою красоту и говорит: «О, бля, опять асинхронщина. Ща я из этого сделаю конечный автомат».

Он тупо разбивает твой метод на куски: до первого await, между await, после последнего. Каждый кусок — это состояние. А переход между ними происходит, когда асинхронная операция завершается.

Зачем это всё? Да чтобы твой код выглядел как обычный, последовательный, а под капотом он мог встать на паузу, не блокируя весь поток, и потом красиво продолжить с нужного места. Магия, сука! Но магия, которая сводится к той же самой таблице состояний и переходов.

Короче, выгода в чём:

  • Прозрачность. Весь жизненный цикл объекта — как на ладони. Не будет такого, что заказ «Доставлен», а его можно ещё раз оплатить.
  • Тестируемость. Каждый переход — отдельно протестировал, и спи спокойно.
  • Стабильность. Невозможно попасть в невалидное состояние, если его нет в таблице. Компьютер тебя просто не поймёт, и слава богу.

Вот и весь сказ. Не бойся этого слова «паттерн». Это просто способ не выстрелить себе в ногу, когда логика становится сложнее, чем «включил-выключил».