machine - Ejemplo de máquina de estado simple en C#?
state machine unity c# (18)
Actualizar:
Nuevamente, gracias por los ejemplos, han sido de gran ayuda y con el siguiente no quiero quitarles nada.
¿No son los ejemplos dados actualmente, en la medida en que los entiendo y a las máquinas de estado, solo la mitad de lo que generalmente entendemos por una máquina de estado?
En el sentido de que los ejemplos cambian de estado, pero eso solo se representa cambiando el valor de una variable (y permitiendo diferentes cambios de valor en diferentes estados), mientras que generalmente una máquina de estado también debería cambiar su comportamiento, y el comportamiento no (solo) en la sensación de permitir diferentes cambios de valor para una variable dependiendo del estado, pero en el sentido de permitir que se ejecuten diferentes métodos para diferentes estados.
¿O tengo una idea errónea de las máquinas de estado y su uso común?
Atentamente
Pregunta original:
Encontré esta discusión sobre las máquinas de estado y los bloques de iteradores en c # y herramientas para crear máquinas de estado y lo que no es para C #, así que encontré muchas cosas abstractas, pero como noob, todo esto es un poco confuso.
Por lo tanto, sería fantástico si alguien pudiera proporcionar un código fuente de C #, por ejemplo, que realice una máquina de estados simple con quizás 3,4 estados, solo para comprender su esencia.
Acabo de aportar esto:
https://code.google.com/p/ysharp/source/browse/#svn%2Ftrunk%2FStateMachinesPoC
Este es uno de los ejemplos que muestran el envío directo e indirecto de comandos, con estados como IObserver (de señal), y por lo tanto responden a una fuente de señal, IObservable (de señal):
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();
}
}
}
Nota: este ejemplo es bastante artificial y, en su mayoría, está destinado a demostrar una serie de características ortogonales. Rara vez debería haber una necesidad real de implementar el propio dominio de valor estatal mediante una clase completa, utilizando el CRTP (ver: http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern ) de esta manera.
Aquí está para un caso de uso de implementación ciertamente más simple y probablemente mucho más común (utilizando un tipo de enumeración simple como el dominio de valor de estados), para la misma máquina de estados y con el mismo caso de prueba:
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();
}
}
}
''HTH
Aquí hay un ejemplo de una máquina de estados finitos muy clásica, modelando un dispositivo electrónico muy simplificado (como un televisor)
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; }
}
}
}
Aquí hay una auto-promoción descarada, pero hace un tiempo creé una biblioteca llamada YieldMachine que permite que una máquina de estados de complejidad limitada se describa de una manera muy limpia y simple. Por ejemplo, considere una lámpara:
Observe que esta máquina de estados tiene 2 disparadores y 3 estados. En el código de YieldMachine, escribimos un método único para todos los comportamientos relacionados con el estado, en el que cometemos la horrible atrocidad de usar goto
para cada estado. Un activador se convierte en una propiedad o campo de tipo Action
, decorado con un atributo llamado Trigger
. He comentado el código del primer estado y sus transiciones a continuación; los siguientes estados siguen el mismo patrón.
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();
}
}
Corto y agradable, eh!
Esta máquina de estados se controla simplemente mediante el envío de activadores:
var sm = new Lamp();
sm.PressSwitch(); //go on
sm.PressSwitch(); //go off
sm.PressSwitch(); //go on
sm.GotError(); //get error
sm.PressSwitch(); //go off
Solo para aclarar, he agregado algunos comentarios al primer estado para ayudarlo a entender cómo usar esto.
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
...
Esto funciona porque el compilador de C # en realidad creó una máquina de estado internamente para cada método que utiliza yield return
. Esta construcción generalmente se usa para crear perezosamente secuencias de datos, pero en este caso no estamos realmente interesados en la secuencia devuelta (que de todos modos es nula), sino en el comportamiento del estado que se crea bajo el capó.
La clase base de StateMachine
reflexiona sobre la construcción para asignar código a cada acción [Trigger]
, que establece el miembro Trigger
y mueve la máquina de estados hacia adelante.
Pero realmente no es necesario entender los elementos internos para poder usarlo.
Empecemos con este simple diagrama de estado:
Tenemos:
- 4 estados (inactivo, activo, pausado y salido)
- 5 tipos de transiciones de estado (Comando de inicio, Comando de finalización, Comando de pausa, Comando de reanudación, Comando de salida).
Puede convertir esto a C # de varias maneras, como realizar una instrucción de cambio en el estado y comando actuales, o buscar transiciones en una tabla de transición. Para esta máquina de estado simple, prefiero una tabla de transición, que es muy fácil de representar utilizando un 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();
}
}
}
Como cuestión de preferencia personal, me gusta diseñar mis máquinas de estado con una función GetNext
para devolver el siguiente estado de manera deterministically , y una función MoveNext
para mutar la máquina de estados.
Encontré este gran tutorial en línea y me ayudó a envolver mi cabeza en máquinas de estados finitos.
El tutorial es independiente del lenguaje, por lo que puede adaptarse fácilmente a sus necesidades de C #.
Además, el ejemplo utilizado (una hormiga que busca comida) es fácil de entender.
Desde el tutorial:
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();
}
(...)
}
Es útil recordar que las máquinas de estado son una abstracción, y no necesita herramientas particulares para crear una, sin embargo, las herramientas pueden ser útiles.
Por ejemplo, puedes realizar una máquina de estados con funciones:
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);
}
Esta máquina cazaría gaviotas e intentaría golpearlas con globos de agua. Si falla, intentará disparar uno hasta que golpee (podría hacerlo con algunas expectativas realistas;), de lo contrario, se regodeará en la consola. Sigue cazando hasta que se acaben las gaviotas para acosar.
Cada función corresponde a cada estado; los estados de inicio y finalización (o aceptación ) no se muestran. Sin embargo, probablemente haya más estados allí que los modelados por las funciones. Por ejemplo, después de disparar el globo, la máquina está realmente en otro estado de lo que era antes, pero decidí que esta distinción no era práctica.
Una forma común es usar clases para representar estados y luego conectarlas de diferentes maneras.
Es posible que desee utilizar una de las máquinas de estado finito de código abierto existentes. Por ejemplo, bbv.Common.StateMachine se encuentra en http://code.google.com/p/bbvcommon/wiki/StateMachine . Tiene una sintaxis fluida muy intuitiva y muchas características tales como, acciones de entrada / salida, acciones de transición, guardias, jerárquica, implementación pasiva (ejecutada en el hilo de la persona que llama) e implementación activa (hilo propio en el que se ejecuta fsm, los eventos se agregan a una cola).
Tomando el ejemplo de Juliets, la definición de la máquina de estado es muy fácil:
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);
Actualización : la ubicación del proyecto se ha movido a: https://github.com/appccelerate/statemachine
Estoy publicando otra respuesta, ya que se trata de máquinas de estado desde una perspectiva diferente; muy visual
Mi respuesta original es el código imperitivo clásico. Creo que es bastante visual a medida que el código va hacia la matriz, lo que simplifica la visualización de la máquina de estados. El inconveniente es que tienes que escribir todo esto. La respuesta de evita el esfuerzo de escribir el código de la placa de la caldera, pero es mucho menos visual. Existe la tercera alternativa; Realmente dibujando la máquina de estados.
Si está utilizando .NET y puede apuntar a la versión 4 del tiempo de ejecución, tiene la opción de usar las actividades de la máquina de estado del flujo de trabajo . En esencia, estos le permiten dibujar la máquina de estados (como en el diagrama de ) y hacer que el tiempo de ejecución de WF lo ejecute por usted.
Consulte el artículo de MSDN Construyendo máquinas de estado con Windows Workflow Foundation para obtener más detalles y este sitio de CodePlex para la última versión.
Esa es la opción que siempre preferiría al apuntar a .NET porque es fácil de ver, cambiar y explicar a los no programadores; Las imágenes valen más que mil palabras como dicen!
Hoy he profundizado en el patrón de diseño de estado. Hice y probé ThreadState, que es igual a (+/-) a Threading en C #, como se describe en la imagen de Threading en C #
Puede agregar fácilmente nuevos estados, configurar movimientos de un estado a otro es muy fácil, ya que se incluye en la implementación del estado
Implementación y uso en: Implementa .NET ThreadState por patrón de diseño de estado
No he intentado implementar un FSM en C # todavía, pero todo esto suena (o parece) muy complicado a la forma en que manejé los FSM en el pasado en lenguajes de bajo nivel como C o ASM.
Creo que el método que siempre he conocido se llama algo así como un "bucle iterativo". En él, esencialmente tiene un bucle ''while'' que sale periódicamente en función de los eventos (interrupciones), y luego vuelve al bucle principal de nuevo.
Dentro de los controladores de interrupción, pasaría un CurrentState y devolvería un NextState, que luego sobrescribe la variable CurrentState en el bucle principal. Usted hace este anuncio infinito hasta que el programa se cierre (o se reinicie el microcontrolador).
En mi opinión, todas las respuestas parecen muy complicadas en comparación con la forma en que se pretende implementar un FSM; Su belleza radica en su simplicidad y la FSM puede ser muy complicada con muchos, muchos estados y transiciones, pero permite que el proceso complicado se descomponga y digiera fácilmente.
Me doy cuenta de que mi respuesta no debería incluir otra pregunta, pero me veo forzado a preguntar: ¿por qué estas otras soluciones propuestas parecen ser tan complicadas?
Parece que son similares a golpear un pequeño clavo con un martillo gigante.
Puede codificar un bloque de iterador que le permite ejecutar un bloque de código de forma orquestada. La forma en que se divide el bloque de código realmente no tiene que corresponder a nada, es solo la forma en que desea codificarlo. Por ejemplo:
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");
}
En este caso, cuando llama a CountToTen, todavía no se ejecuta nada. Lo que obtiene es efectivamente un generador de máquina de estado, para el que puede crear una nueva instancia de la máquina de estado. Lo haces llamando a GetEnumerator (). El IEnumerator resultante es efectivamente una máquina de estado que puede manejar llamando a MoveNext (...).
Por lo tanto, en este ejemplo, la primera vez que llame a MoveNext (...) verá "1" escrito en la consola, y la próxima vez que llame a MoveNext (...) verá 2, 3, 4 y luego 5, 6, 7 y luego 8, y luego 9, 10. Como puede ver, es un mecanismo útil para organizar cómo deben ocurrir las cosas.
Qué combate StatePattern. ¿Se ajusta eso a tus necesidades?
Creo que su contexto está relacionado, pero vale la pena intentarlo.
http://en.wikipedia.org/wiki/State_pattern
Esto le permite a sus estados decidir a dónde ir y no a la clase "objeto".
Bruno
Hay 2 paquetes de máquinas de estado populares en NuGet.
https://github.com/appccelerate/statemachine (13.6K descargas + 3.82K de versión heredada (bbv.Common.StateMachine))
StateMachineToolkit (1.56K descargas)
La biblioteca de Appccelerate tiene buena documentación , pero no es compatible con .NET 4, así que elegí StateMachineToolkit para mi proyecto.
Creo que la máquina de estado propuesta por Juliet tiene un error: el método GetHashCode puede devolver el mismo código hash para dos transiciones diferentes, por ejemplo:
Estado = Activo (1), Comando = Pausa (2) => HashCode = 17 + 31 + 62 = 110
Estado = Pausado (2), Comando = Fin (1) => HashCode = 17 + 62 + 31 = 110
Para evitar este error, el método debería ser así:
public override int GetHashCode()
{
return 17 + 23 * CurrentState.GetHashCode() + 31 * Command.GetHashCode();
}
Alex
En mi opinión, una máquina de estados no solo está destinada a estados cambiantes sino también (muy importante) para manejar desencadenantes / eventos dentro de un estado específico. Si desea comprender mejor el patrón de diseño de la máquina de estados, puede encontrar una buena descripción en el libro Head First Design Patterns, página 320 .
No se trata solo de los estados dentro de las variables, sino también del manejo de disparadores dentro de los diferentes estados. Gran capítulo (y no, no hay ninguna tarifa para mí al mencionar esto :-) que contiene una explicación fácil de entender.
FiniteStateMachine es una máquina de estado simple, escrita en C # Link
Ventajas de usar mi biblioteca FiniteStateMachine:
- Defina una clase de "contexto" para presentar una interfaz única al mundo exterior.
- Definir una clase base abstracta de estado.
- Representa los diferentes "estados" de la máquina de estado como clases derivadas de la clase base de estado.
- Defina el comportamiento específico del estado en las clases apropiadas derivadas del estado.
- Mantenga un puntero al "estado" actual en la clase "contexto".
- Para cambiar el estado de la máquina de estado, cambie el puntero actual del "estado".
Descargar DLL Download
Ejemplo en 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;
}
}
Hice esta máquina de estado genérica a partir del código de Juliet. Está funcionando genial para mí.
Estos son los beneficios:
- puede crear una nueva máquina de estado en código con dos enumeraciones
TState
yTCommand
, - Se agregó una estructura
TransitionResult<TState>
para tener más control sobre los resultados de los[Try]GetNext()
métodos. - exponiendo clase anidada
StateTransition
únicamente a través deAddTransition(TState, TCommand, TState)
lo que es más fácil trabajar con él
Código:
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;
}
}
Este es el tipo de retorno del método TryGetNext:
public struct TransitionResult<TState>
{
public TransitionResult(TState newState, bool isValid)
{
NewState = newState;
IsValid = isValid;
}
public TState NewState;
public bool IsValid;
}
Cómo utilizar:
Así es como puedes crear una OnlineDiscountStateMachine
clase genérica:
Defina una enumeración OnlineDiscountState
para sus estados y una enumeración OnlineDiscountCommand
para sus comandos.
Defina una clase OnlineDiscountStateMachine
derivada de la clase genérica usando esas dos enumeraciones
Derive el constructor de base(OnlineDiscountState.InitialState)
modo que el estado inicial se establezca enOnlineDiscountState.InitialState
Utilice AddTransition
tantas veces como sea necesario
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);
}
}
usar la máquina de estado derivada
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);
}
Yo recomendaría state.cs . Personalmente utilicé state.js (la versión de JavaScript) y estoy muy contento con él. Esa versión C # funciona de manera similar.
Usted instancia los estados:
// 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" );
...
Usted instancia algunas transiciones:
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" ) );
Se definen acciones sobre estados y transiciones:
t0.Effect += DisengageHead;
t0.Effect += StopMotor;
Y eso es (bastante) eso. Mira el sitio web para más información.