Простой пример конечного автомата в C#?
Обновить:
Еще раз спасибо за примеры, они были очень полезны, и со следующим я не хочу ничего от них отнимать.
Разве приведенные в настоящее время примеры, насколько я понимаю их и конечных автоматов, не являются лишь половиной того, что мы обычно понимаем под конечным автоматом?
В том смысле, что примеры действительно меняют состояние, но это представлено только изменением значения переменной (и разрешением разных изменений значения в разных состояниях), в то время как обычно конечный автомат также должен изменять свое поведение, а поведение не (только) в смысл разрешения различных изменений значений для переменной в зависимости от состояния, но в смысле разрешения выполнения различных методов для разных состояний.
Или у меня неправильное представление о конечных автоматах и их общем использовании?
С наилучшими пожеланиями
Оригинальный вопрос:
Я нашел это обсуждение о конечных автоматах и блоках итераторов в C# и инструментах для создания конечных автоматов, а не о C#, так что я нашел много абстрактных вещей, но в качестве нуба все это немного сбивает с толку.
Поэтому было бы замечательно, если бы кто-то мог предоставить пример исходного кода C#, который реализует простой конечный автомат, возможно, с 3,4 состояниями, просто чтобы понять его суть.
24 ответа
Давайте начнем с этой простой диаграммы состояний:
У нас есть:
- 4 состояния (неактивно, активно, приостановлено и вышло)
- 5 типов переходов между состояниями (команда "Начало", "Команда конца", "Команда паузы", "Команда возобновления", "Команда выхода").
Вы можете преобразовать это в C# несколькими способами, такими как выполнение оператора switch для текущего состояния и команды или поиск переходов в таблице переходов. Для этого простого конечного автомата я предпочитаю таблицу переходов, которую очень легко представить с помощью Dictionary
:
using System;
using System.Collections.Generic;
namespace Juliet
{
public enum ProcessState
{
Inactive,
Active,
Paused,
Terminated
}
public enum Command
{
Begin,
End,
Pause,
Resume,
Exit
}
public class Process
{
class StateTransition
{
readonly ProcessState CurrentState;
readonly Command Command;
public StateTransition(ProcessState currentState, Command command)
{
CurrentState = currentState;
Command = command;
}
public override int GetHashCode()
{
return 17 + 31 * CurrentState.GetHashCode() + 31 * Command.GetHashCode();
}
public override bool Equals(object obj)
{
StateTransition other = obj as StateTransition;
return other != null && this.CurrentState == other.CurrentState && this.Command == other.Command;
}
}
Dictionary<StateTransition, ProcessState> transitions;
public ProcessState CurrentState { get; private set; }
public Process()
{
CurrentState = ProcessState.Inactive;
transitions = new Dictionary<StateTransition, ProcessState>
{
{ new StateTransition(ProcessState.Inactive, Command.Exit), ProcessState.Terminated },
{ new StateTransition(ProcessState.Inactive, Command.Begin), ProcessState.Active },
{ new StateTransition(ProcessState.Active, Command.End), ProcessState.Inactive },
{ new StateTransition(ProcessState.Active, Command.Pause), ProcessState.Paused },
{ new StateTransition(ProcessState.Paused, Command.End), ProcessState.Inactive },
{ new StateTransition(ProcessState.Paused, Command.Resume), ProcessState.Active }
};
}
public ProcessState GetNext(Command command)
{
StateTransition transition = new StateTransition(CurrentState, command);
ProcessState nextState;
if (!transitions.TryGetValue(transition, out nextState))
throw new Exception("Invalid transition: " + CurrentState + " -> " + command);
return nextState;
}
public ProcessState MoveNext(Command command)
{
CurrentState = GetNext(command);
return CurrentState;
}
}
public class Program
{
static void Main(string[] args)
{
Process p = new Process();
Console.WriteLine("Current State = " + p.CurrentState);
Console.WriteLine("Command.Begin: Current State = " + p.MoveNext(Command.Begin));
Console.WriteLine("Command.Pause: Current State = " + p.MoveNext(Command.Pause));
Console.WriteLine("Command.End: Current State = " + p.MoveNext(Command.End));
Console.WriteLine("Command.Exit: Current State = " + p.MoveNext(Command.Exit));
Console.ReadLine();
}
}
}
Из личных предпочтений мне нравится проектировать свои конечные автоматы с GetNext
функция для возврата следующего состояния детерминистически, и MoveNext
функция для изменения состояния конечного автомата.
Возможно, вы захотите использовать один из существующих конечных автоматов с открытым исходным кодом. Например, bbv.Common.StateMachine можно найти по адресу http://code.google.com/p/bbvcommon/wiki/StateMachine. Он имеет очень интуитивно понятный свободный синтаксис и множество функций, таких как действия входа / выхода, действия перехода, охранники, иерархическая, пассивная реализация (выполняется в потоке вызывающей стороны) и активная реализация (собственный поток, в котором работает fsm, события добавляются в очередь).
На примере Джульетта определение конечного автомата становится очень простым:
var fsm = new PassiveStateMachine<ProcessState, Command>();
fsm.In(ProcessState.Inactive)
.On(Command.Exit).Goto(ProcessState.Terminated).Execute(SomeTransitionAction)
.On(Command.Begin).Goto(ProcessState.Active);
fsm.In(ProcessState.Active)
.ExecuteOnEntry(SomeEntryAction)
.ExecuteOnExit(SomeExitAction)
.On(Command.End).Goto(ProcessState.Inactive)
.On(Command.Pause).Goto(ProcessState.Paused);
fsm.In(ProcessState.Paused)
.On(Command.End).Goto(ProcessState.Inactive).OnlyIf(SomeGuard)
.On(Command.Resume).Goto(ProcessState.Active);
fsm.Initialize(ProcessState.Inactive);
fsm.Start();
fsm.Fire(Command.Begin);
Обновление: местоположение проекта перемещено по адресу: https://github.com/appccelerate/statemachine
Вот пример очень классического конечного автомата, моделирующего очень упрощенное электронное устройство (например, телевизор)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace fsm
{
class Program
{
static void Main(string[] args)
{
var fsm = new FiniteStateMachine();
Console.WriteLine(fsm.State);
fsm.ProcessEvent(FiniteStateMachine.Events.PlugIn);
Console.WriteLine(fsm.State);
fsm.ProcessEvent(FiniteStateMachine.Events.TurnOn);
Console.WriteLine(fsm.State);
fsm.ProcessEvent(FiniteStateMachine.Events.TurnOff);
Console.WriteLine(fsm.State);
fsm.ProcessEvent(FiniteStateMachine.Events.TurnOn);
Console.WriteLine(fsm.State);
fsm.ProcessEvent(FiniteStateMachine.Events.RemovePower);
Console.WriteLine(fsm.State);
Console.ReadKey();
}
class FiniteStateMachine
{
public enum States { Start, Standby, On };
public States State { get; set; }
public enum Events { PlugIn, TurnOn, TurnOff, RemovePower };
private Action[,] fsm;
public FiniteStateMachine()
{
this.fsm = new Action[3, 4] {
//PlugIn, TurnOn, TurnOff, RemovePower
{this.PowerOn, null, null, null}, //start
{null, this.StandbyWhenOff, null, this.PowerOff}, //standby
{null, null, this.StandbyWhenOn, this.PowerOff} }; //on
}
public void ProcessEvent(Events theEvent)
{
this.fsm[(int)this.State, (int)theEvent].Invoke();
}
private void PowerOn() { this.State = States.Standby; }
private void PowerOff() { this.State = States.Start; }
private void StandbyWhenOn() { this.State = States.Standby; }
private void StandbyWhenOff() { this.State = States.On; }
}
}
}
Некоторая бесстыдная самореклама здесь, но некоторое время назад я создал библиотеку под названием YieldMachine, которая позволяет описать конечный автомат ограниченной сложности очень простым и понятным способом. Например, рассмотрим лампу:
Обратите внимание, что этот конечный автомат имеет 2 триггера и 3 состояния. В коде YieldMachine мы пишем единый метод для всего поведения, связанного с состоянием, в котором мы совершаем ужасное злодеяние использования goto
для каждого государства. Триггер становится свойством или полем типа Action
, украшенный атрибутом под названием Trigger
, Я прокомментировал код первого состояния и его переходы ниже; следующие состояния следуют той же схеме.
public class Lamp : StateMachine
{
// Triggers (or events, or actions, whatever) that our
// state machine understands.
[Trigger]
public readonly Action PressSwitch;
[Trigger]
public readonly Action GotError;
// Actual state machine logic
protected override IEnumerable WalkStates()
{
off:
Console.WriteLine("off.");
yield return null;
if (Trigger == PressSwitch) goto on;
InvalidTrigger();
on:
Console.WriteLine("*shiiine!*");
yield return null;
if (Trigger == GotError) goto error;
if (Trigger == PressSwitch) goto off;
InvalidTrigger();
error:
Console.WriteLine("-err-");
yield return null;
if (Trigger == PressSwitch) goto off;
InvalidTrigger();
}
}
Коротко и красиво, а!
Этот конечный автомат управляется простой отправкой ему триггеров:
var sm = new Lamp();
sm.PressSwitch(); //go on
sm.PressSwitch(); //go off
sm.PressSwitch(); //go on
sm.GotError(); //get error
sm.PressSwitch(); //go off
Просто чтобы уточнить, я добавил несколько комментариев к первому состоянию, чтобы помочь вам понять, как это использовать.
protected override IEnumerable WalkStates()
{
off: // Each goto label is a state
Console.WriteLine("off."); // State entry actions
yield return null; // This means "Wait until a
// trigger is called"
// Ah, we got triggered!
// perform state exit actions
// (none, in this case)
if (Trigger == PressSwitch) goto on; // Transitions go here:
// depending on the trigger
// that was called, go to
// the right state
InvalidTrigger(); // Throw exception on
// invalid trigger
...
Это работает, потому что компилятор C# фактически создал конечный автомат для каждого метода, который использует yield return
, Эта конструкция обычно используется для ленивого создания последовательностей данных, но в этом случае нас на самом деле не интересует возвращаемая последовательность (которая в любом случае равна нулю), а поведение поведения, которое создается внутри.
StateMachine
Базовый класс делает некоторые размышления о конструкции, чтобы назначить код каждому [Trigger]
действие, которое устанавливает Trigger
член и двигает конечный автомат вперед.
Но вам не нужно понимать внутренности, чтобы иметь возможность использовать их.
Вы можете кодировать блок итератора, который позволяет вам выполнить блок кода организованным способом. То, как блок кода разбит, на самом деле ни к чему не должно соответствовать, просто то, как вы хотите его кодировать. Например:
IEnumerable<int> CountToTen()
{
System.Console.WriteLine("1");
yield return 0;
System.Console.WriteLine("2");
System.Console.WriteLine("3");
System.Console.WriteLine("4");
yield return 0;
System.Console.WriteLine("5");
System.Console.WriteLine("6");
System.Console.WriteLine("7");
yield return 0;
System.Console.WriteLine("8");
yield return 0;
System.Console.WriteLine("9");
System.Console.WriteLine("10");
}
В этом случае, когда вы вызываете CountToTen, на самом деле ничего еще не выполняется. По сути, вы получаете генератор конечных автоматов, для которого вы можете создать новый экземпляр конечного автомата. Вы делаете это, вызывая GetEnumerator(). Получающийся IEnumerator фактически является конечным автоматом, который вы можете использовать, вызывая MoveNext(...).
Таким образом, в этом примере при первом вызове MoveNext (...) вы увидите "1", записанное в консоль, а при следующем вызове MoveNext (...) вы увидите 2, 3, 4 и затем 5, 6, 7, а затем 8, а затем 9, 10. Как видите, это полезный механизм для организации того, как все должно происходить.
Полезно помнить, что конечные автоматы являются абстракцией, и вам не нужны специальные инструменты для их создания, однако инструменты могут быть полезны.
Например, вы можете реализовать конечный автомат с функциями:
void Hunt(IList<Gull> gulls)
{
if (gulls.Empty())
return;
var target = gulls.First();
TargetAcquired(target, gulls);
}
void TargetAcquired(Gull target, IList<Gull> gulls)
{
var balloon = new WaterBalloon(weightKg: 20);
this.Cannon.Fire(balloon);
if (balloon.Hit)
{
TargetHit(target, gulls);
}
else
TargetMissed(target, gulls);
}
void TargetHit(Gull target, IList<Gull> gulls)
{
Console.WriteLine("Suck on it {0}!", target.Name);
Hunt(gulls);
}
void TargetMissed(Gull target, IList<Gull> gulls)
{
Console.WriteLine("I'll get ya!");
TargetAcquired(target, gulls);
}
Эта машина охотилась на чаек и пыталась поразить их водяными шарами. Если он пропустит, он будет пытаться выстрелить до тех пор, пока не доберется (может, с некоторыми реалистичными ожиданиями;)), иначе он будет злорадствовать в консоли. Это продолжает охотиться, пока это не из чаек, чтобы изводить.
Каждая функция соответствует каждому состоянию; состояния начала и окончания (или принятия) не показаны. Хотя там, вероятно, больше состояний, чем моделируется функциями. Например, после запуска воздушного шара машина действительно находится в другом состоянии, чем было до нее, но я решил, что сделать это различие нецелесообразно.
Обычный способ - использовать классы для представления состояний, а затем соединять их различными способами.
Нашел этот замечательный учебник в Интернете, и он помог мне обернуть голову в конечные автоматы.
Учебное пособие не зависит от языка, поэтому его можно легко адаптировать к вашим потребностям в C#.
Кроме того, используемый пример (муравей ищет еду) легко понять.
Из учебника:
public class FSM {
private var activeState :Function; // points to the currently active state function
public function FSM() {
}
public function setState(state :Function) :void {
activeState = state;
}
public function update() :void {
if (activeState != null) {
activeState();
}
}
}
public class Ant
{
public var position :Vector3D;
public var velocity :Vector3D;
public var brain :FSM;
public function Ant(posX :Number, posY :Number) {
position = new Vector3D(posX, posY);
velocity = new Vector3D( -1, -1);
brain = new FSM();
// Tell the brain to start looking for the leaf.
brain.setState(findLeaf);
}
/**
* The "findLeaf" state.
* It makes the ant move towards the leaf.
*/
public function findLeaf() :void {
// Move the ant towards the leaf.
velocity = new Vector3D(Game.instance.leaf.x - position.x, Game.instance.leaf.y - position.y);
if (distance(Game.instance.leaf, this) <= 10) {
// The ant is extremelly close to the leaf, it's time
// to go home.
brain.setState(goHome);
}
if (distance(Game.mouse, this) <= MOUSE_THREAT_RADIUS) {
// Mouse cursor is threatening us. Let's run away!
// It will make the brain start calling runAway() from
// now on.
brain.setState(runAway);
}
}
/**
* The "goHome" state.
* It makes the ant move towards its home.
*/
public function goHome() :void {
// Move the ant towards home
velocity = new Vector3D(Game.instance.home.x - position.x, Game.instance.home.y - position.y);
if (distance(Game.instance.home, this) <= 10) {
// The ant is home, let's find the leaf again.
brain.setState(findLeaf);
}
}
/**
* The "runAway" state.
* It makes the ant run away from the mouse cursor.
*/
public function runAway() :void {
// Move the ant away from the mouse cursor
velocity = new Vector3D(position.x - Game.mouse.x, position.y - Game.mouse.y);
// Is the mouse cursor still close?
if (distance(Game.mouse, this) > MOUSE_THREAT_RADIUS) {
// No, the mouse cursor has gone away. Let's go back looking for the leaf.
brain.setState(findLeaf);
}
}
public function update():void {
// Update the FSM controlling the "brain". It will invoke the currently
// active state function: findLeaf(), goHome() or runAway().
brain.update();
// Apply the velocity vector to the position, making the ant move.
moveBasedOnVelocity();
}
(...)
}
Я публикую здесь другой ответ, так как это конечные автоматы с другой точки зрения; очень наглядно
Мой оригинальный ответ - классический неуязвимый код. Я думаю, что это довольно визуально, поскольку код идет из-за массива, который делает визуализацию конечного автомата простой. Недостатком является то, что вы должны написать все это. Ответ Ремоса облегчает работу по написанию кода, но гораздо менее нагляден. Есть третья альтернатива; действительно рисующий конечный автомат.
Если вы используете.NET и можете ориентироваться на версию 4 среды выполнения, у вас есть возможность использовать действия конечного автомата рабочего процесса. По сути, они позволяют вам нарисовать конечный автомат (так же, как на диаграмме Juliet) и заставить среду выполнения WF выполнить его для вас.
Для получения дополнительной информации см. Статью MSDN " Создание конечных автоматов с помощью Windows Workflow Foundation" и этот сайт CodePlex для последней версии.
Это вариант, который я всегда предпочел бы при нацеливании на.NET, потому что его легко увидеть, изменить и объяснить не программистам; картинки стоят тысячи слов как говорится!
Сегодня я в глубине государства Design Pattern. Я сделал и протестировал ThreadState, который равен (+/-) Threading в C#, как описано на рисунке из Threading в C#
Вы можете легко добавлять новые состояния, настраивать переходы из одного состояния в другое очень легко, потому что оно включено в реализацию состояний
Внедрение и использование в: Реализует.NET ThreadState с помощью State Design Pattern
Я еще не пробовал внедрять FSM в C#, но все это звучит (или выглядит) очень сложно по сравнению с тем, как я обрабатывал FSM в прошлом на языках низкого уровня, таких как C или ASM.
Я считаю, что метод, который я всегда знал, называется чем-то вроде "Итеративного цикла". В нем, по сути, имеется цикл while, который периодически выходит на основе событий (прерываний), а затем снова возвращается в основной цикл.
В обработчиках прерываний вы должны передать CurrentState и вернуть NextState, который затем перезаписывает переменную CurrentState в главном цикле. Вы делаете это до бесконечности, пока программа не закроется (или микроконтроллер не перезагрузится).
То, что я вижу, другие ответы выглядят очень сложными по сравнению с тем, как, на мой взгляд, ФШМ должен быть реализован; Его красота заключается в его простоте, и FSM может быть очень сложным со многими, многими состояниями и переходами, но они позволяют легко разбивать и переваривать сложный процесс.
Я понимаю, что мой ответ не должен включать другой вопрос, но я вынужден спросить: почему эти другие предлагаемые решения кажутся такими сложными?
Похоже, они сродни ударам маленького гвоздя гигантской кувалдой.
Не уверен, упустил ли я суть дела, но я думаю, что ни один из ответов здесь не является "простым" конечным автоматом. То, что я обычно называю простым конечным автоматом, использует цикл с переключателем внутри. Это то, что мы использовали в программировании ПЛК / микрочипов или в программировании на C/C++ в университете.
преимущества:
- легко писать. никаких специальных предметов и прочего не требуется. для этого даже не нужна объектная ориентация.
- когда он маленький, это легко понять.
недостатки:
- при большом количестве состояний может стать довольно большим и трудным для чтения.
Выглядело это так:
public enum State
{
First,
Second,
Third,
}
static void Main(string[] args)
{
var state = State.First;
// x and i are just examples for stuff that you could change inside the state and use for state transitions
var x = 0;
var i = 0;
// does not have to be a while loop. you could loop over the characters of a string too
while (true)
{
switch (state)
{
case State.First:
// Do sth here
if (x == 2)
state = State.Second;
// you may or may not add a break; right after setting the next state
// or do sth here
if (i == 3)
state = State.Third;
// or here
break;
case State.Second:
// Do sth here
if (x == 10)
state = State.First;
// or do sth here
break;
case State.Third:
// Do sth here
if (x == 10)
state = State.First;
// or do sth here
break;
default:
// you may wanna throw an exception here.
break;
}
}
}
если это действительно должен быть конечный автомат, на котором вы вызываете методы, которые по-разному реагируют в зависимости от того, в каком состоянии вы находитесь: шаблон проектирования состояний - лучший подход
Я сделал этот универсальный конечный автомат из кода Джульетты. Это работает потрясающе для меня.
Вот эти преимущества:
- Вы можете создать новый конечный автомат в коде с двумя перечислениями
TState
а такжеTCommand
, - добавлена структура
TransitionResult<TState>
иметь больший контроль над выходными результатами[Try]GetNext()
методы - разоблачение вложенного класса
StateTransition
только черезAddTransition(TState, TCommand, TState)
облегчая работу с ним
Код:
public class StateMachine<TState, TCommand>
where TState : struct, IConvertible, IComparable
where TCommand : struct, IConvertible, IComparable
{
protected class StateTransition<TS, TC>
where TS : struct, IConvertible, IComparable
where TC : struct, IConvertible, IComparable
{
readonly TS CurrentState;
readonly TC Command;
public StateTransition(TS currentState, TC command)
{
if (!typeof(TS).IsEnum || !typeof(TC).IsEnum)
{
throw new ArgumentException("TS,TC must be an enumerated type");
}
CurrentState = currentState;
Command = command;
}
public override int GetHashCode()
{
return 17 + 31 * CurrentState.GetHashCode() + 31 * Command.GetHashCode();
}
public override bool Equals(object obj)
{
StateTransition<TS, TC> other = obj as StateTransition<TS, TC>;
return other != null
&& this.CurrentState.CompareTo(other.CurrentState) == 0
&& this.Command.CompareTo(other.Command) == 0;
}
}
private Dictionary<StateTransition<TState, TCommand>, TState> transitions;
public TState CurrentState { get; private set; }
protected StateMachine(TState initialState)
{
if (!typeof(TState).IsEnum || !typeof(TCommand).IsEnum)
{
throw new ArgumentException("TState,TCommand must be an enumerated type");
}
CurrentState = initialState;
transitions = new Dictionary<StateTransition<TState, TCommand>, TState>();
}
/// <summary>
/// Defines a new transition inside this state machine
/// </summary>
/// <param name="start">source state</param>
/// <param name="command">transition condition</param>
/// <param name="end">destination state</param>
protected void AddTransition(TState start, TCommand command, TState end)
{
transitions.Add(new StateTransition<TState, TCommand>(start, command), end);
}
public TransitionResult<TState> TryGetNext(TCommand command)
{
StateTransition<TState, TCommand> transition = new StateTransition<TState, TCommand>(CurrentState, command);
TState nextState;
if (transitions.TryGetValue(transition, out nextState))
return new TransitionResult<TState>(nextState, true);
else
return new TransitionResult<TState>(CurrentState, false);
}
public TransitionResult<TState> MoveNext(TCommand command)
{
var result = TryGetNext(command);
if(result.IsValid)
{
//changes state
CurrentState = result.NewState;
}
return result;
}
}
Это тип возврата метода TryGetNext:
public struct TransitionResult<TState>
{
public TransitionResult(TState newState, bool isValid)
{
NewState = newState;
IsValid = isValid;
}
public TState NewState;
public bool IsValid;
}
Как пользоваться:
Вот как вы можете создать OnlineDiscountStateMachine
из общего класса:
Определить перечисление OnlineDiscountState
для его состояний и перечисления OnlineDiscountCommand
для своих команд.
Определить класс OnlineDiscountStateMachine
происходит от общего класса, используя эти два перечисления
Получите конструктор из base(OnlineDiscountState.InitialState)
так что начальное состояние установлено на OnlineDiscountState.InitialState
использование AddTransition
столько раз, сколько нужно
public class OnlineDiscountStateMachine : StateMachine<OnlineDiscountState, OnlineDiscountCommand>
{
public OnlineDiscountStateMachine() : base(OnlineDiscountState.Disconnected)
{
AddTransition(OnlineDiscountState.Disconnected, OnlineDiscountCommand.Connect, OnlineDiscountState.Connected);
AddTransition(OnlineDiscountState.Disconnected, OnlineDiscountCommand.Connect, OnlineDiscountState.Error_AuthenticationError);
AddTransition(OnlineDiscountState.Connected, OnlineDiscountCommand.Submit, OnlineDiscountState.WaitingForResponse);
AddTransition(OnlineDiscountState.WaitingForResponse, OnlineDiscountCommand.DataReceived, OnlineDiscountState.Disconnected);
}
}
использовать производный конечный автомат
odsm = new OnlineDiscountStateMachine();
public void Connect()
{
var result = odsm.TryGetNext(OnlineDiscountCommand.Connect);
//is result valid?
if (!result.IsValid)
//if this happens you need to add transitions to the state machine
//in this case result.NewState is the same as before
Console.WriteLine("cannot navigate from this state using OnlineDiscountCommand.Connect");
//the transition was successfull
//show messages for new states
else if(result.NewState == OnlineDiscountState.Error_AuthenticationError)
Console.WriteLine("invalid user/pass");
else if(result.NewState == OnlineDiscountState.Connected)
Console.WriteLine("Connected");
else
Console.WriteLine("not implemented transition result for " + result.NewState);
}
По моему мнению, конечный автомат предназначен не только для изменения состояний, но также (очень важно) для обработки триггеров / событий в определенном состоянии. Если вы хотите лучше понять шаблон проектирования конечного автомата, хорошее описание можно найти в книге Head First Design Patterns, стр. 320.
Речь идет не только о состояниях внутри переменных, но и об обработке триггеров в различных состояниях. Отличная глава (и нет, упоминать об этом мне не нужно:-), которая содержит просто понятное объяснение.
Я только что добавил это:
https://code.google.com/p/ysharp/source/browse/
Вот один из примеров, демонстрирующих прямую и непрямую отправку команд с состояниями как IObserver(для сигнала), таким образом, отвечает на источник сигнала, IObservable(для сигнала):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Test
{
using Machines;
public static class WatchingTvSampleAdvanced
{
// Enum type for the transition triggers (instead of System.String) :
public enum TvOperation { Plug, SwitchOn, SwitchOff, Unplug, Dispose }
// The state machine class type is also used as the type for its possible states constants :
public class Television : NamedState<Television, TvOperation, DateTime>
{
// Declare all the possible states constants :
public static readonly Television Unplugged = new Television("(Unplugged TV)");
public static readonly Television Off = new Television("(TV Off)");
public static readonly Television On = new Television("(TV On)");
public static readonly Television Disposed = new Television("(Disposed TV)");
// For convenience, enter the default start state when the parameterless constructor executes :
public Television() : this(Television.Unplugged) { }
// To create a state machine instance, with a given start state :
private Television(Television value) : this(null, value) { }
// To create a possible state constant :
private Television(string moniker) : this(moniker, null) { }
private Television(string moniker, Television value)
{
if (moniker == null)
{
// Build the state graph programmatically
// (instead of declaratively via custom attributes) :
Handler<Television, TvOperation, DateTime> stateChangeHandler = StateChange;
Build
(
new[]
{
new { From = Television.Unplugged, When = TvOperation.Plug, Goto = Television.Off, With = stateChangeHandler },
new { From = Television.Unplugged, When = TvOperation.Dispose, Goto = Television.Disposed, With = stateChangeHandler },
new { From = Television.Off, When = TvOperation.SwitchOn, Goto = Television.On, With = stateChangeHandler },
new { From = Television.Off, When = TvOperation.Unplug, Goto = Television.Unplugged, With = stateChangeHandler },
new { From = Television.Off, When = TvOperation.Dispose, Goto = Television.Disposed, With = stateChangeHandler },
new { From = Television.On, When = TvOperation.SwitchOff, Goto = Television.Off, With = stateChangeHandler },
new { From = Television.On, When = TvOperation.Unplug, Goto = Television.Unplugged, With = stateChangeHandler },
new { From = Television.On, When = TvOperation.Dispose, Goto = Television.Disposed, With = stateChangeHandler }
},
false
);
}
else
// Name the state constant :
Moniker = moniker;
Start(value ?? this);
}
// Because the states' value domain is a reference type, disallow the null value for any start state value :
protected override void OnStart(Television value)
{
if (value == null)
throw new ArgumentNullException("value", "cannot be null");
}
// When reaching a final state, unsubscribe from all the signal source(s), if any :
protected override void OnComplete(bool stateComplete)
{
// Holds during all transitions into a final state
// (i.e., stateComplete implies IsFinal) :
System.Diagnostics.Debug.Assert(!stateComplete || IsFinal);
if (stateComplete)
UnsubscribeFromAll();
}
// Executed before and after every state transition :
private void StateChange(IState<Television> state, ExecutionStep step, Television value, TvOperation info, DateTime args)
{
// Holds during all possible transitions defined in the state graph
// (i.e., (step equals ExecutionStep.LeaveState) implies (not state.IsFinal))
System.Diagnostics.Debug.Assert((step != ExecutionStep.LeaveState) || !state.IsFinal);
// Holds in instance (i.e., non-static) transition handlers like this one :
System.Diagnostics.Debug.Assert(this == state);
switch (step)
{
case ExecutionStep.LeaveState:
var timeStamp = ((args != default(DateTime)) ? String.Format("\t\t(@ {0})", args) : String.Empty);
Console.WriteLine();
// 'value' is the state value that we are transitioning TO :
Console.WriteLine("\tLeave :\t{0} -- {1} -> {2}{3}", this, info, value, timeStamp);
break;
case ExecutionStep.EnterState:
// 'value' is the state value that we have transitioned FROM :
Console.WriteLine("\tEnter :\t{0} -- {1} -> {2}", value, info, this);
break;
default:
break;
}
}
public override string ToString() { return (IsConstant ? Moniker : Value.ToString()); }
}
public static void Run()
{
Console.Clear();
// Create a signal source instance (here, a.k.a. "remote control") that implements
// IObservable<TvOperation> and IObservable<KeyValuePair<TvOperation, DateTime>> :
var remote = new SignalSource<TvOperation, DateTime>();
// Create a television state machine instance (automatically set in a default start state),
// and make it subscribe to a compatible signal source, such as the remote control, precisely :
var tv = new Television().Using(remote);
bool done;
// Always holds, assuming the call to Using(...) didn't throw an exception (in case of subscription failure) :
System.Diagnostics.Debug.Assert(tv != null, "There's a bug somewhere: this message should never be displayed!");
// As commonly done, we can trigger a transition directly on the state machine :
tv.MoveNext(TvOperation.Plug, DateTime.Now);
// Alternatively, we can also trigger transitions by emitting from the signal source / remote control
// that the state machine subscribed to / is an observer of :
remote.Emit(TvOperation.SwitchOn, DateTime.Now);
remote.Emit(TvOperation.SwitchOff);
remote.Emit(TvOperation.SwitchOn);
remote.Emit(TvOperation.SwitchOff, DateTime.Now);
done =
(
tv.
MoveNext(TvOperation.Unplug).
MoveNext(TvOperation.Dispose) // MoveNext(...) returns null iff tv.IsFinal == true
== null
);
remote.Emit(TvOperation.Unplug); // Ignored by the state machine thanks to the OnComplete(...) override above
Console.WriteLine();
Console.WriteLine("Is the TV's state '{0}' a final state? {1}", tv.Value, done);
Console.WriteLine();
Console.WriteLine("Press any key...");
Console.ReadKey();
}
}
}
Примечание: этот пример довольно искусственный и в основном предназначен для демонстрации ряда ортогональных функций. Редко возникает реальная необходимость реализовать сам домен значений состояния с помощью полноценного класса, используя CRTP (см. Http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern), как это.
Вот для гораздо более простого и, вероятно, гораздо более распространенного варианта использования реализации (с использованием простого типа enum в качестве области значений состояний), для того же конечного автомата и с тем же тестовым примером:
https://code.google.com/p/ysharp/source/browse/trunk/StateMachinesPoC/WatchingTVSample.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Test
{
using Machines;
public static class WatchingTvSample
{
public enum Status { Unplugged, Off, On, Disposed }
public class DeviceTransitionAttribute : TransitionAttribute
{
public Status From { get; set; }
public string When { get; set; }
public Status Goto { get; set; }
public object With { get; set; }
}
// State<Status> is a shortcut for / derived from State<Status, string>,
// which in turn is a shortcut for / derived from State<Status, string, object> :
public class Device : State<Status>
{
// Executed before and after every state transition :
protected override void OnChange(ExecutionStep step, Status value, string info, object args)
{
if (step == ExecutionStep.EnterState)
{
// 'value' is the state value that we have transitioned FROM :
Console.WriteLine("\t{0} -- {1} -> {2}", value, info, this);
}
}
public override string ToString() { return Value.ToString(); }
}
// Since 'Device' has no state graph of its own, define one for derived 'Television' :
[DeviceTransition(From = Status.Unplugged, When = "Plug", Goto = Status.Off)]
[DeviceTransition(From = Status.Unplugged, When = "Dispose", Goto = Status.Disposed)]
[DeviceTransition(From = Status.Off, When = "Switch On", Goto = Status.On)]
[DeviceTransition(From = Status.Off, When = "Unplug", Goto = Status.Unplugged)]
[DeviceTransition(From = Status.Off, When = "Dispose", Goto = Status.Disposed)]
[DeviceTransition(From = Status.On, When = "Switch Off", Goto = Status.Off)]
[DeviceTransition(From = Status.On, When = "Unplug", Goto = Status.Unplugged)]
[DeviceTransition(From = Status.On, When = "Dispose", Goto = Status.Disposed)]
public class Television : Device { }
public static void Run()
{
Console.Clear();
// Create a television state machine instance, and return it, set in some start state :
var tv = new Television().Start(Status.Unplugged);
bool done;
// Holds iff the chosen start state isn't a final state :
System.Diagnostics.Debug.Assert(tv != null, "The chosen start state is a final state!");
// Trigger some state transitions with no arguments
// ('args' is ignored by this state machine's OnChange(...), anyway) :
done =
(
tv.
MoveNext("Plug").
MoveNext("Switch On").
MoveNext("Switch Off").
MoveNext("Switch On").
MoveNext("Switch Off").
MoveNext("Unplug").
MoveNext("Dispose") // MoveNext(...) returns null iff tv.IsFinal == true
== null
);
Console.WriteLine();
Console.WriteLine("Is the TV's state '{0}' a final state? {1}", tv.Value, done);
Console.WriteLine();
Console.WriteLine("Press any key...");
Console.ReadKey();
}
}
}
"НТН
Какой бой StatePattern. Это соответствует вашим потребностям?
Я думаю, что его контекст связан, но его стоит попробовать точно.
http://en.wikipedia.org/wiki/State_pattern
Это позволит вашим штатам решать, куда идти, а не "объектному" классу.
Bruno
Я думаю, что конечный автомат, предложенный Джульеттой, имеет ошибку: метод GetHashCode может возвращать один и тот же хеш-код для двух разных переходов, например:
Состояние = активный (1), команда = пауза (2) => HashCode = 17 + 31 + 62 = 110
State = Paused (2), Command = End (1) => HashCode = 17 + 62 + 31 = 110
Чтобы избежать этой ошибки, метод должен быть таким:
public override int GetHashCode()
{
return 17 + 23 * CurrentState.GetHashCode() + 31 * Command.GetHashCode();
}
Alex
Другая альтернатива в этом репо https://github.com/lingkodsoft/StateBliss использует свободный синтаксис, поддерживает триггеры.
public class BasicTests
{
[Fact]
public void Tests()
{
// Arrange
StateMachineManager.Register(new [] { typeof(BasicTests).Assembly }); //Register at bootstrap of your application, i.e. Startup
var currentState = AuthenticationState.Unauthenticated;
var nextState = AuthenticationState.Authenticated;
var data = new Dictionary<string, object>();
// Act
var changeInfo = StateMachineManager.Trigger(currentState, nextState, data);
// Assert
Assert.True(changeInfo.StateChangedSucceeded);
Assert.Equal("ChangingHandler1", changeInfo.Data["key1"]);
Assert.Equal("ChangingHandler2", changeInfo.Data["key2"]);
}
//this class gets regitered automatically by calling StateMachineManager.Register
public class AuthenticationStateDefinition : StateDefinition<AuthenticationState>
{
public override void Define(IStateFromBuilder<AuthenticationState> builder)
{
builder.From(AuthenticationState.Unauthenticated).To(AuthenticationState.Authenticated)
.Changing(this, a => a.ChangingHandler1)
.Changed(this, a => a.ChangedHandler1);
builder.OnEntering(AuthenticationState.Authenticated, this, a => a.OnEnteringHandler1);
builder.OnEntered(AuthenticationState.Authenticated, this, a => a.OnEnteredHandler1);
builder.OnExiting(AuthenticationState.Unauthenticated, this, a => a.OnExitingHandler1);
builder.OnExited(AuthenticationState.Authenticated, this, a => a.OnExitedHandler1);
builder.OnEditing(AuthenticationState.Authenticated, this, a => a.OnEditingHandler1);
builder.OnEdited(AuthenticationState.Authenticated, this, a => a.OnEditedHandler1);
builder.ThrowExceptionWhenDiscontinued = true;
}
private void ChangingHandler1(StateChangeGuardInfo<AuthenticationState> changeinfo)
{
var data = changeinfo.DataAs<Dictionary<string, object>>();
data["key1"] = "ChangingHandler1";
}
private void OnEnteringHandler1(StateChangeGuardInfo<AuthenticationState> changeinfo)
{
// changeinfo.Continue = false; //this will prevent changing the state
}
private void OnEditedHandler1(StateChangeInfo<AuthenticationState> changeinfo)
{
}
private void OnExitedHandler1(StateChangeInfo<AuthenticationState> changeinfo)
{
}
private void OnEnteredHandler1(StateChangeInfo<AuthenticationState> changeinfo)
{
}
private void OnEditingHandler1(StateChangeGuardInfo<AuthenticationState> changeinfo)
{
}
private void OnExitingHandler1(StateChangeGuardInfo<AuthenticationState> changeinfo)
{
}
private void ChangedHandler1(StateChangeInfo<AuthenticationState> changeinfo)
{
}
}
public class AnotherAuthenticationStateDefinition : StateDefinition<AuthenticationState>
{
public override void Define(IStateFromBuilder<AuthenticationState> builder)
{
builder.From(AuthenticationState.Unauthenticated).To(AuthenticationState.Authenticated)
.Changing(this, a => a.ChangingHandler2);
}
private void ChangingHandler2(StateChangeGuardInfo<AuthenticationState> changeinfo)
{
var data = changeinfo.DataAs<Dictionary<string, object>>();
data["key2"] = "ChangingHandler2";
}
}
}
public enum AuthenticationState
{
Unauthenticated,
Authenticated
}
}
Я бы порекомендовал state.cs. Я лично использовал state.js (версия JavaScript) и очень доволен этим. Эта версия C# работает аналогичным образом.
Вы создаете экземпляры состояний:
// create the state machine
var player = new StateMachine<State>( "player" );
// create some states
var initial = player.CreatePseudoState( "initial", PseudoStateKind.Initial );
var operational = player.CreateCompositeState( "operational" );
...
Вы создаете несколько переходов:
var t0 = player.CreateTransition( initial, operational );
player.CreateTransition( history, stopped );
player.CreateTransition<String>( stopped, running, ( state, command ) => command.Equals( "play" ) );
player.CreateTransition<String>( active, stopped, ( state, command ) => command.Equals( "stop" ) );
Вы определяете действия над состояниями и переходами:
t0.Effect += DisengageHead;
t0.Effect += StopMotor;
И это (в значительной степени) это. Посмотрите на сайте для получения дополнительной информации.
FiniteStateMachine - это простой конечный автомат, написанный на C# Link.
Преимущества использования моей библиотеки FiniteStateMachine:
- Определите "контекстный" класс для представления единого интерфейса с внешним миром.
- Определите состояние абстрактного базового класса.
- Представлять различные "состояния" конечного автомата как производные классы базового класса State.
- Определите поведение, зависящее от состояния, в соответствующих классах, производных от состояния.
- Поддерживать указатель на текущее состояние в классе context.
- Чтобы изменить состояние конечного автомата, измените текущий указатель "состояния".
Скачать DLL Скачать
Пример на LINQPad:
void Main()
{
var machine = new SFM.Machine(new StatePaused());
var output = machine.Command("Input_Start", Command.Start);
Console.WriteLine(Command.Start.ToString() + "-> State: " + machine.Current);
Console.WriteLine(output);
output = machine.Command("Input_Pause", Command.Pause);
Console.WriteLine(Command.Pause.ToString() + "-> State: " + machine.Current);
Console.WriteLine(output);
Console.WriteLine("-------------------------------------------------");
}
public enum Command
{
Start,
Pause,
}
public class StateActive : SFM.State
{
public override void Handle(SFM.IContext context)
{
//Gestione parametri
var input = (String)context.Input;
context.Output = input;
//Gestione Navigazione
if ((Command)context.Command == Command.Pause) context.Next = new StatePaused();
if ((Command)context.Command == Command.Start) context.Next = this;
}
}
public class StatePaused : SFM.State
{
public override void Handle(SFM.IContext context)
{
//Gestione parametri
var input = (String)context.Input;
context.Output = input;
//Gestione Navigazione
if ((Command)context.Command == Command.Start) context.Next = new StateActive();
if ((Command)context.Command == Command.Pause) context.Next = this;
}
}
В NuGet есть 2 популярных пакета конечных автоматов.
https://github.com/appccelerate/statemachine (13.6K загрузок + 3.82K устаревшей версии (bbv.Common.StateMachine))
StateMachineToolkit (1.56K загрузок)
В библиотеке Appccelerate lib есть хорошая документация, но она не поддерживает.NET 4, поэтому я выбрал StateMachineToolkit для своего проекта.
Еще один конечный автомат для списка, мой: https://github.com/IanMercer/Abodit.StateMachine
В дополнение к простым состояниям с действиями входа и выхода и действиями при каждом переходе, это предназначено для использования в асинхронном коде. Он также поддерживает иерархические состояния и машины со сложными состояниями. Так что это не совсем «просто», но в использовании довольно легко кодировать состояния и переходы.
static OpenClosedStateMachine()
{
Closed
.When(Fridge.eDoorOpens, (m, s, e, c) => Task.FromResult(Open));
Open
.When(Fridge.eDoorCloses, (m, s, e, c) => Task.FromResult(Closed));
}
В отличие от других, он также поддерживает временные переходы, поэтому легко перейти в другое состояние.
After
данный период или
At
заданное время.
Я создал библиотеку Nuget, которая реализует простой и мощный конечный автомат и может быть внедрена в DI. Вы можете проверить это здесь Nuget - State Machine
Можете воспользоваться моим решением, это самый удобный способ. Это тоже бесплатно.
Создайте конечный автомат за три шага:
1. Создайте схему в редакторе узлов и загрузите ее в свой проект с помощью библиотеки.
StateMachine stateMachine = новый StateMachine ("scheme.xml");
2. Опишите логику вашего приложения на событиях⚡
stateMachine.GetState ("State1"). OnExit (Action1); stateMachine.GetState ("State2"). OnEntry (Action2); stateMachine.GetTransition("Переход1").OnInvoke(Действие3); stateMachine.OnChangeState(Action4);
3. Запустите конечный автомат.
stateMachine.Start();
Ссылки:
Редактор узлов: https://github.com/SimpleStateMachine/SimpleStateMachineNodeEditor
Библиотека: https://github.com/SimpleStateMachine/SimpleStateMachineLibrary
Я тоже боролся с конечными автоматами при использовании с брокером сообщений, таким как RabbitMQ или Rabbit.
Я создал это видео, чтобы помочь другим.
https://www.youtube.com/watch?v=Vwfngk0YhLs&amp;t=11s&amp;ab_channel=ГарриТейлор
Код был из этого репозитория Github