Ejemplo de máquina de estado simple en C #?

258

Actualizar:

Nuevamente, gracias por los ejemplos, han sido muy útiles y con lo siguiente no pretendo quitarles nada.

¿No son los ejemplos que se dan actualmente, por lo que yo entiendo y las máquinas de estados, solo la mitad de lo que generalmente entendemos por una máquina de estados?
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 estados también debe cambiar su comportamiento, y el comportamiento no (solo) en el sentido 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 máquinas de estado y bloques iteradores en C # y herramientas para crear máquinas de estado y qué no para C #, así que encontré muchas cosas abstractas, pero como novato, todo esto es un poco confuso.

Por lo tanto, sería genial si alguien pudiera proporcionar un código fuente de ejemplo de C # que se dé cuenta de una máquina de estado simple con quizás 3,4 estados, solo para entenderlo.


Jennifer Owens
fuente
¿Te preguntas sobre las máquinas de estado en general o solo las basadas en iteradores?
Skurmedel
2
Existe .Net Core Stateless lib con ejemplos, daigram de DAG, etc. - vale la pena revisar: hanselman.com/blog/…
zmische

Respuestas:

416

Comencemos con este simple diagrama de estado:

diagrama de máquina de estado simple

Tenemos:

  • 4 estados (inactivo, activo, en pausa 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 declaració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 con 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 GetNextfunción para devolver el siguiente estado de manera determinista , y una MoveNextfunción para mutar la máquina de estado.

Julieta
fuente
66
+1 para la correcta implementación del GetHashCode()uso de primos.
ja72
13
¿Podría explicarme el propósito de GetHashCode ()?
Siddharth
14
@Siddharth: la StateTransitionclase se usa como clave en el diccionario y la igualdad de claves es importante. Dos instancias distintas de StateTransitiondeben considerarse iguales siempre que representen la misma transición (por ejemplo, CurrentStatey Commandsean iguales). Para implementar la igualdad, debe anular Equalstambién GetHashCode. En particular, el diccionario usará el código hash y dos objetos iguales deben devolver el mismo código hash. También obtiene un buen rendimiento si no demasiados objetos no iguales comparten el mismo código hash, por lo que GetHashCodese implementa como se muestra.
Martin Liversage
14
Si bien esto seguramente le brinda una máquina de estado (y una implementación adecuada de C # 'ish también), ¿siento que todavía falta la respuesta a la pregunta del OP sobre el cambio de comportamiento? Después de todo, solo calcula estados, pero aún falta el comportamiento relacionado con los cambios de estado, la carne real del programa y generalmente llamados eventos de Entrada / Salida.
stijn
2
Si alguien lo va a necesitar: ajusté esta máquina de tate y la usé en mi juego de la unidad. Está disponible en git hub: github.com/MarcoMig/Finite-State-Machine-FSM
Max_Power89
73

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 como, acciones de entrada / salida, acciones de transición, guardias, implementación jerárquica, pasiva (ejecutada en el hilo de la persona que llama) e implementación activa (propio hilo en el que se ejecuta el fsm, los eventos se agregan a una cola).

Tomando el ejemplo de Juliets, la definición de la máquina de estados se vuelve 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

Remo Gloor
fuente
44
Gracias por hacer referencia a esta excelente máquina de estado de código abierto. ¿Puedo preguntar cómo puedo obtener el estado actual?
Ramazan Polat
3
No puedes y no deberías. El estado es algo inestable. Cuando solicita el estado, es posible que se encuentre en medio de una transición. Todas las acciones deben realizarse dentro de las transiciones, entrada de estado y salidas de estado. Si realmente desea tener el estado, puede agregar un campo local y asignar el estado en una acción de entrada.
Remo Gloor
44
La pregunta es para qué lo "necesita" y si realmente necesita el estado SM o algún otro tipo de estado. Por ejemplo, si necesita algún texto de visualización, varios indicados podrían tener el mismo texto de visualización, por ejemplo, si la preparación para el envío tiene múltiples estados secundarios. En este caso, debe hacer exactamente lo que pretende hacer. Actualice algunos textos de visualización en los lugares correctos. Por ejemplo, dentro de ExecuteOnEntry. Si necesita más información, haga una nueva pregunta y exponga exactamente su problema, ya que esto está saliendo del tema aquí.
Remo Gloor
Ok, estoy haciendo una nueva pregunta y esperando que respondas. Porque no creo que alguien más resuelva este problema, ya que tiene la mejor respuesta, pero aún así el interrogador no aceptó. Publicaré la URL de la pregunta aquí. Gracias.
Ramazan Polat
44
+1 para la API fluida y declarativa. Es impresionante. Por cierto, el código de google parece estar desactualizado. Su sitio de proyecto más nuevo está en GitHub aquí
KFL
52

Aquí hay un ejemplo de una máquina de estados finitos muy clásica, que modela 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; }
    }
}
}
Pete Stensønes
fuente
66
Para cualquier persona nueva en máquinas de estado, este es un excelente primer ejemplo para mojarse primero los pies.
PositiveGuy
2
Soy nuevo en las máquinas de estado y, en serio, esto me ha traído The Light, ¡gracias!
MC5
1
Me gustó esta implementación. Para cualquiera que pueda tropezar con esto, una ligera "mejora". En la clase FSM, agregué private void DoNothing() {return;}y reemplacé todas las instancias de null con this.DoNothing. Tiene el efecto secundario agradable de devolver el estado actual.
Sethmo011
1
Me pregunto si hay un razonamiento detrás de algunos de estos nombres. Cuando miro esto, mi primera intuición es renombrar los elementos del Statesto Unpowered, Standby, On. Mi razonamiento es que si alguien me preguntara en qué estado está mi televisor, diría "Apagado" y no "Inicio". También cambié StandbyWhenOny StandbyWhenOffpara TurnOny TurnOff. Eso hace que el código se lea de manera más intuitiva, pero me pregunto si hay convenciones u otros factores que hacen que mi terminología sea menos apropiada.
Jason Hamje el
Parece razonable, realmente no estaba siguiendo ninguna convención de nomenclatura estatal; nombre como tiene sentido para lo que sea que modeles.
Pete Stensønes
20

Aquí hay una autopromoción descarada, pero hace un tiempo creé una biblioteca llamada YieldMachine que permite que una máquina de estado de complejidad limitada se describa de una manera muy limpia y simple. Por ejemplo, considere una lámpara:

máquina de estado de 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 gotopara 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 enviándole disparadores:

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 comprender 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 # realmente creó una máquina de estado internamente para cada método que utiliza yield return. Esta construcción generalmente se usa para crear secuencias de datos de manera perezosa, pero en este caso no estamos realmente interesados ​​en la secuencia devuelta (que de todos modos es nula), sino en el comportamiento de estado que se crea bajo el capó.

La StateMachineclase base reflexiona sobre la construcción para asignar código a cada [Trigger]acción, lo que establece el Triggermiembro y mueve la máquina de estado hacia adelante.

Pero en realidad no necesitas entender las partes internas para poder usarlo.

skrebbel
fuente
2
El "goto" solo es atroz si salta entre métodos. Eso, afortunadamente, no está permitido en C #.
Brannon
¡Buen punto! De hecho, estaría muy impresionado si algún lenguaje estáticamente escrito lograra permitir un gotométodo entre métodos.
skrebbel
3
@Brannon: ¿qué idioma permite gotosaltar entre métodos? No veo cómo podría funcionar eso. No, gotoes problemático porque da como resultado una programación procesal (esto en sí mismo complica cosas buenas como las pruebas unitarias), promueve la repetición del código (¿se dio cuenta de cómo InvalidTriggerdebe insertarse para cada estado?) Y finalmente hace que el programa fluya más difícil de seguir. Compare esto con (la mayoría) de otras soluciones en este hilo y verá que esta es la única en la que ocurre todo el FSM en un solo método. Eso suele ser suficiente para plantear una preocupación.
Groo
1
@Groo, GW-BASIC, por ejemplo. Ayuda que no tenga métodos, ni siquiera funciones. Además de eso, me cuesta mucho entender por qué encuentra el "flujo del programa más difícil de seguir" en este ejemplo. Es una máquina de estados, "ir a" un estado desde otro es lo único que haces. Esto se asigna gotobastante bien.
skrebbel
3
¿GW-BASIC permite gotosaltar entre funciones, pero no admite funciones? :) Tienes razón, el comentario "más difícil de seguir" es más un gotoproblema general , de hecho no es un gran problema en este caso.
Groo
13

Puede codificar un bloque iterador que le permite ejecutar un bloque de código de manera orquestada. La forma en que se divide el bloque de código realmente no tiene que corresponder a nada, es solo cómo 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 llamas a CountToTen, todavía no se ejecuta nada. Lo que obtienes es efectivamente un generador de máquina de estado, para el cual puedes crear una nueva instancia de la máquina de estado. Para ello, llame 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 orquestar cómo deberían ocurrir las cosas.

Kevin Hsu
fuente
66
Enlace obligatorio a la advertencia justa
sehe
8

Estoy publicando otra respuesta aquí, ya que se trata de máquinas de estado desde una perspectiva diferente; Muy visual.

Mi respuesta original es el código imperativo clásico. Creo que es bastante visual en cuanto al código debido a la matriz que simplifica la visualización de la máquina de estados. La desventaja es que tienes que escribir todo esto. La respuesta de Remos alivia el esfuerzo de escribir el código de la caldera, pero es mucho menos visual. Existe la tercera alternativa; Realmente dibujando la máquina de estado.

Si está usando .NET y puede apuntar a la versión 4 del tiempo de ejecución, entonces tiene la opción de usar las actividades de la máquina de estado del flujo de trabajo . En esencia, esto le permite dibujar la máquina de estado (como en el diagrama de Juliet ) y hacer que el tiempo de ejecución de WF lo ejecute por usted.

Consulte el artículo de MSDN Building State Machines with Windows Workflow Foundation para obtener más detalles y este sitio de CodePlex para obtener la última versión.

Esa es la opción que siempre preferiría cuando apunte a .NET porque es fácil de ver, cambiar y explicar a quienes no son programadores; ¡las imágenes valen más que mil palabras como dicen!

Pete Stensønes
fuente
¡Creo que la máquina de estados es una de las mejores partes de toda la base del flujo de trabajo!
fabsenet
7

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, puede realizar una máquina de estado 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 buscarí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. Continúa cazando hasta que se quedan sin 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 un estado diferente al que tenía antes, pero decidí que esta distinción no era práctica.

Una forma común es usar clases para representar estados y luego conectarlos de diferentes maneras.

Skurmedel
fuente
7

Encontré este gran tutorial en línea y me ayudó a comprender las máquinas de estados finitos.

http://gamedevelopment.tutsplus.com/tutorials/finite-state-machines-theory-and-implementation--gamedev-11867

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 buscando comida) es fácil de entender.


Del tutorial:

ingrese la descripción de la imagen aquí

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();
    }

    (...)
}
Azul azabache
fuente
1
Si bien este enlace puede responder la pregunta, es mejor incluir aquí las partes esenciales de la respuesta y proporcionar el enlace como referencia. Las respuestas de solo enlace pueden dejar de ser válidas si la página vinculada cambia. - De la opinión
drneel
@drneel Podría copiar y pegar bits del tutorial ... pero ¿eso no le quitaría crédito al autor?
Jet Blue
1
@JetBlue: deje el enlace en la respuesta como referencia e incluya los bits relevantes en sus propias palabras en la publicación de respuesta para no romper los derechos de autor de nadie. Sé que parece estricto, pero muchas respuestas se han vuelto mucho, mucho mejores debido a esta regla.
Flimm
6

Hoy estoy profundamente en el patrón de diseño estatal. Lo hice y probé ThreadState, que equivale (+/-) a Threading en C #, como se describe en la imagen de Threading en C #

ingrese la descripción de la imagen aquí

Puede agregar fácilmente nuevos estados, configurar movimientos de un estado a otro es muy fácil porque está encapsulado en la implementación del estado

Implementación y uso en: Implementa .NET ThreadState por patrón de diseño estatal

zzfima
fuente
2
Link está muerto. ¿Tienes otro?
lanza el
5

Todavía no he intentado implementar un FSM en C #, pero todo esto suena (o parece) muy complicado por 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 se cierra periódicamente en función de los eventos (interrupciones), luego vuelve al bucle principal nuevamente.

Dentro de los manejadores de interrupciones, pasaría un CurrentState y devolvería un NextState, que luego sobrescribe la variable CurrentState en el bucle principal. Hace esto ad infinitum hasta que el programa se cierra (o el microcontrolador se reinicia).

Lo que veo en otras respuestas parece muy complicado en comparación con cómo, en mi opinión, se pretende implementar un FSM; su belleza radica en su simplicidad y FSM puede ser muy complicado con muchos, muchos estados y transiciones, pero permiten 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 obligado a preguntar: ¿por qué estas otras soluciones propuestas parecen ser tan complicadas?
Parecen ser como golpear un pequeño clavo con un martillo gigante.

dluberger
fuente
1
Completamente de acuerdo. Un ciclo while simple con una instrucción switch es lo más simple posible.
lanza el
2
A menos que tenga una máquina de estados muy complicada con muchos estados y condiciones, donde terminaría con múltiples conmutadores anidados. También puede haber una penalización en espera ocupada, dependiendo de la implementación de su ciclo.
Sune Rievers
3

Qué pelea StatePattern. ¿Se ajusta eso a tus necesidades?

Creo que está relacionado con el contexto, pero vale la pena intentarlo.

http://en.wikipedia.org/wiki/State_pattern

Esto permite que sus estados decidan a dónde ir y no la clase de "objeto".

Bruno

Bruno Bertechini
fuente
1
El patrón de estado trata con una clase que puede actuar de manera diferente en función del estado / modo en el que se encuentra, no trata con la transición entre estados.
Eli Algranti
3

En mi opinión, una máquina de estados no solo está diseñada para cambiar estados, 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 de manejar los desencadenantes dentro de los diferentes estados. Gran capítulo (y no, no hay que pagar nada por mencionar esto :-) que contiene una explicación fácil de entender.

Ton Snoei
fuente
3

Acabo de contribuir con 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), por lo tanto, responde 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 está destinado principalmente a demostrar una serie de características ortogonales. Rara vez debería existir una necesidad real de implementar el dominio de valor de estado en sí mismo mediante una clase completa, utilizando el CRTP (ver: http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern ) como este.

Aquí hay un caso de uso de implementación ciertamente más simple y probablemente mucho más común (usando un tipo de enumeración simple como 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

YSharp
fuente
¿No es un poco extraño que cada instancia de estado tenga su propia copia del gráfico de estado?
Groo
@Groo: no, no lo hacen. Solo las instancias de Televisión construidas usando el constructor privado con una cadena nula para el apodo (por lo tanto, llamando al método protegido 'Construir') tendrán un gráfico de estado, como máquinas de estado. Los otros, instancias nombradas de Televisión (con un apodo no nulo para ese propósito convencional y ad-hoc) serán simples estados de "punto fijo" (por así decirlo), que servirán como las constantes de estado (que los gráficos de estado de las máquinas de estado reales harán referencia como sus vértices). 'HTH,
YSharp
Ok, lo entiendo. De todos modos, en mi humilde opinión, hubiera sido mejor si hubiera incluido algún código que realmente maneja estas transiciones. De esta manera, solo sirve como un ejemplo del uso de una interfaz no tan obvia (en mi humilde opinión) para su biblioteca. Por ejemplo, ¿cómo se StateChangeresuelve? A través de la reflexión? ¿Es eso realmente necesario?
Groo
1
@Groo: Buen comentario. De hecho, no es necesario reflexionar sobre el controlador en ese primer ejemplo porque se hace programáticamente allí con precisión y se puede vincular estáticamente / verificar el tipo (a diferencia de cuando se hace a través de atributos personalizados). Por lo tanto este trabajo como se esperaba demasiado: private Television(string moniker, Television value) { Handler<Television, TvOperation, DateTime> myHandler = StateChange; // (code omitted) new { From = Television.Unplugged, When = TvOperation.Plug, Goto = Television.Off, With = myHandler } }
YSharp
1
¡Gracias por tu esfuerzo!
Groo
3

Hice esta máquina de estado genérica con el código de Juliet. Me está funcionando de maravilla.

Estos son los beneficios:

  • puedes crear una nueva máquina de estados en código con dos enumeraciones TStatey TCommand,
  • estructura agregada TransitionResult<TState>para tener más control sobre los resultados de salida de los [Try]GetNext()métodos
  • exponiendo clase anidada StateTransition únicamente a través de AddTransition(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 puede crear un a OnlineDiscountStateMachinepartir de la clase genérica:

Defina una enumeración OnlineDiscountStatepara sus estados y una enumeración OnlineDiscountCommandpara sus comandos.

Definir una clase OnlineDiscountStateMachinederivada 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

Use AddTransitiontantas 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);
    }
Bizhan
fuente
1

Creo que la máquina de estados 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 = En pausa (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

alexag
fuente
1
No se requiere el código hash para devolver un número único para cualquier combinación posible, solo un valor distinto con una buena distribución en todo el rango objetivo (en este caso, el rango son todos los intvalores posibles ). Es por eso HashCodeque siempre se implementa junto con Equals. Si los códigos hash son los mismos, se verifica la equidad exacta de los objetos utilizando el Equalsmétodo
Dmitry Avtonomov
0

FiniteStateMachine es una máquina de estado simple, escrita en C # Link

Ventajas de usar mi biblioteca FiniteStateMachine:

  1. Defina una clase de "contexto" para presentar una única interfaz al mundo exterior.
  2. Definir una clase base abstracta de estado.
  3. Representa los diferentes "estados" de la máquina de estados como clases derivadas de la clase base de estado.
  4. Definir el comportamiento específico del estado en las clases derivadas de estado apropiadas.
  5. Mantenga un puntero al "estado" actual en la clase "contexto".
  6. Para cambiar el estado de la máquina de estado, cambie el puntero de "estado" actual.

Descargar DLL Descargar

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;


     }

 }
Domenico Zinzi
fuente
1
Tiene licencia GNU GPL.
Der_Meister
0

Yo recomendaría state.cs . Personalmente utilicé state.js (la versión de JavaScript) y estoy muy contento con él. Esa versión de C # funciona de manera similar.

Instancias de 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" );
        ...

Instancias 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" ) );

Define acciones en estados y transiciones:

    t0.Effect += DisengageHead;
    t0.Effect += StopMotor;

Y eso es todo. Mira el sitio web para más información.

bmorin
fuente
0

Hay 2 paquetes populares de máquinas de estado en NuGet.

Appccelerate.StateMachine (13.6K descargas + 3.82K de la versión heredada (bbv.Common.StateMachine))

StateMachineToolkit (1.56K descargas)

Appccelerate lib tiene buena documentación , pero no es compatible con .NET 4, así que elegí StateMachineToolkit para mi proyecto.

Der_Meister
fuente
0

Otra alternativa en este repositorio https://github.com/lingkodsoft/StateBliss utilizó una sintaxis fluida, admite disparadores.

    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
    }
}
mcdm
fuente
0

Puedes usar mi solución, esta es la forma más conveniente. También es gratis.

Crear máquina de estados en tres pasos:

1. Cree un esquema en el editor de nodos🔗 y cárguelo en su proyecto usando biblioteca📚

StateMachine stateMachine = new StateMachine ("scheme.xml");

2. Describe la lógica de tu aplicación en eventos⚡

stateMachine.GetState ("State1"). OnExit (Acción1);
stateMachine.GetState ("State2"). OnEntry (Action2);
stateMachine.GetTransition ("Transition1"). OnInvoke (Acción3);
stateMachine.OnChangeState (Action4);

3. Ejecute la máquina de estados🚘

stateMachine.Start ();

Enlaces:

Editor de nodos: https://github.com/SimpleStateMachine/SimpleStateMachineNodeEditor

Biblioteca: https://github.com/SimpleStateMachine/SimpleStateMachineLibrary

GMIKE
fuente