Esempio di macchina a stati semplice in C #?


258

Aggiornare:

Ancora una volta grazie per gli esempi, sono stati molto utili e con quanto segue non intendo togliergli nulla.

Gli esempi attualmente forniti, per quanto li capisco e le macchine a stati, non sono solo la metà di ciò che di solito capiamo da una macchina a stati?
Nel senso che gli esempi cambiano stato ma questo è rappresentato solo cambiando il valore di una variabile (e consentendo cambiamenti di valore diversi in stati diversi), mentre di solito una macchina a stati dovrebbe anche cambiare il suo comportamento, e il comportamento non (solo) in il senso di consentire cambiamenti di valore diversi per una variabile in base allo stato, ma nel senso di consentire l'esecuzione di metodi diversi per stati diversi.

O ho un'idea sbagliata delle macchine statali e del loro uso comune?

I migliori saluti


Domanda originale:

Ho trovato questa discussione su macchine a stati e blocchi di iteratori in c # e strumenti per creare macchine a stati e cosa no per C #, quindi ho trovato un sacco di cose astratte ma come un noob tutto ciò è un po 'confuso.

Quindi sarebbe bello se qualcuno potesse fornire un esempio di codice sorgente C # che realizza una semplice macchina a stati con forse 3,4 stati, solo per farne un'idea.



Ti stai chiedendo delle macchine a stati in generale o solo quelle basate sull'iteratore?
Skurmedel,

2
Esiste .Net Core Stateless lib con esempi, daigram DAG ecc. - vale la pena rivedere: hanselman.com/blog/…
zmische

Risposte:


416

Cominciamo con questo semplice diagramma di stato:

diagramma macchina a stati semplici

Abbiamo:

  • 4 stati (inattivo, attivo, in pausa ed uscito)
  • 5 tipi di transizioni di stato (comando di inizio, comando di fine, comando di pausa, comando di ripresa, comando di uscita).

Puoi convertirlo in C # in diversi modi, ad esempio eseguendo un'istruzione switch sullo stato e sul comando correnti o cercando le transizioni in una tabella di transizione. Per questa semplice macchina a stati, preferisco una tabella di transizione, che è molto facile da rappresentare usando 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();
        }
    }
}

Per quanto riguarda le preferenze personali, mi piace progettare le mie macchine a stati con una GetNextfunzione per restituire lo stato successivo in modo deterministico e una MoveNextfunzione per mutare la macchina a stati.


66
+1 per l'implementazione corretta GetHashCode()dell'uso dei numeri primi.
Ja72,

13
Potresti spiegarmi lo scopo di GetHashCode ()?
Siddharth,

14
@Siddharth: la StateTransitionclasse viene utilizzata come chiave nel dizionario e l'uguaglianza delle chiavi è importante. Due casi distinti di StateTransitiondovrebbero essere considerati uguali purché rappresentino la stessa transizione (ad esempio CurrentStatee Commandsiano gli stessi). Per implementare l'uguaglianza si deve ignorare Equalscosì come GetHashCode. In particolare il dizionario utilizzerà il codice hash e due oggetti uguali devono restituire lo stesso codice hash. Ottieni anche buone prestazioni se non troppi oggetti non uguali condividono lo stesso codice hash, motivo per cui GetHashCodeè implementato come mostrato.
Martin Liversage,

14
Mentre questo sicuramente ti procura una macchina a stati (e anche una corretta implementazione di C #), penso che manchi ancora la risposta alla domanda del PO sul cambiamento del comportamento? Dopotutto, calcola solo gli stati ma manca ancora il comportamento relativo ai cambiamenti di stato, all'effettiva carne del programma e di solito chiamati eventi di entrata / uscita.
Stijn

2
Se qualcuno ne avrà bisogno: ho aggiustato questa macchina per tate e l'ho usata nel mio gioco di unità. È disponibile su git hub: github.com/MarcoMig/Finite-State-Machine-FSM
Max_Power89

73

È possibile che si desideri utilizzare una delle macchine a stati finiti open source esistenti. Ad esempio bbv.Common.StateMachine disponibile su http://code.google.com/p/bbvcommon/wiki/StateMachine . Ha una sintassi fluida molto intuitiva e molte funzionalità come, azioni di invio / uscita, azioni di transizione, protezioni, implementazione gerarchica, passiva (eseguita sul thread del chiamante) e implementazione attiva (thread proprio su cui gira l'FMM, gli eventi vengono aggiunti a una coda).

Prendendo l'esempio di Juliets, la definizione per la macchina a stati diventa molto semplice:

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

Aggiornamento : la posizione del progetto è stata spostata su: https://github.com/appccelerate/statemachine


4
Grazie per aver fatto riferimento a questa eccellente macchina a stati open source. Posso chiedere come posso ottenere lo stato attuale?
Ramazan Polat,

3
Non puoi e non dovresti. Lo stato è qualcosa di instabile. Quando richiedi lo stato, è possibile che ti trovi nel mezzo di una transizione. Tutte le azioni devono essere eseguite all'interno di transizioni, entrate e uscite di stato. Se vuoi davvero avere lo stato, puoi aggiungere un campo locale e assegnare lo stato in un'azione di inserimento.
Remo Gloor,

4
La domanda è per cosa "ne hai bisogno" e se hai davvero bisogno dello stato SM o di qualche altro tipo di stato. Ad esempio, se hai bisogno di un po 'di testo di visualizzazione, molti di questi potrebbero avere lo stesso testo di visualizzazione, ad esempio se la preparazione per l'invio ha più stati secondari. In questo caso dovresti fare esattamente quello che intendi fare. Aggiorna del testo visualizzato nei punti corretti. Ad esempio, all'interno di ExecuteOnEntry. Se hai bisogno di maggiori informazioni, fai una nuova domanda e indica esattamente il tuo problema in quanto questo sta andando fuori tema qui.
Remo Gloor,

Ok, sto facendo una nuova domanda e aspetto che tu risponda. Perché non penso che qualcun altro risolva questo problema poiché hai la risposta migliore ma l'interrogante non ha accettato. Pubblicherò l'URL della domanda qui. Grazie.
Ramazan Polat,

4
+1 per l'API fluente e dichiarativa. È meraviglioso. A proposito, il codice di Google sembra essere obsoleto. Il loro nuovo sito del progetto è su GitHub qui
KFL

52

Ecco un esempio di una macchina a stati finiti molto classica, che modella un dispositivo elettronico molto semplificato (come una TV)

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

6
per chiunque sia nuovo a dichiarare macchine, questo è un ottimo primo esempio per bagnare prima i piedi.
Positivo

2
Sono nuovo di macchine statali e seriamente, questo mi ha portato The Light - grazie!
MC5

1
Mi è piaciuta questa implementazione. Per chiunque possa inciampare in questo, un leggero "miglioramento". Nella classe FSM, ho aggiunto private void DoNothing() {return;}e sostituito tutte le istanze di null con this.DoNothing. Ha il piacevole effetto collaterale di restituire lo stato attuale.
Sethmo011,

1
Mi chiedo se c'è un ragionamento dietro alcuni di questi nomi. Quando guardo a questo, la mia prima intuizione è quella di rinominare gli elementi del Statesto Unpowered, Standby, On. Il mio ragionamento è che se qualcuno mi chiedesse in che stato si trova la mia televisione, direi "Off" e non "Start". Ho anche cambiato StandbyWhenOne StandbyWhenOffin TurnOne TurnOff. Ciò rende il codice letto in modo più intuitivo, ma mi chiedo se ci sono convenzioni o altri fattori che rendono la mia terminologia meno appropriata.
Jason Hamje,

Sembra ragionevole, non stavo davvero seguendo alcuna convenzione di denominazione statale; il nome ha senso per qualunque modello.
Pete Stensønes,

20

Alcuni self-promo spudorati qui, ma qualche tempo fa ho creato una libreria chiamata YieldMachine che consente di descrivere una macchina a stati a complessità limitata in modo molto pulito e semplice. Ad esempio, considera una lampada:

macchina a stati di una lampada

Si noti che questa macchina a stati ha 2 trigger e 3 stati. Nel codice YieldMachine, scriviamo un singolo metodo per tutti i comportamenti relativi allo stato, in cui commettiamo l'atrocità orribile dell'uso gotoper ogni stato. Un trigger diventa una proprietà o un campo di tipo Action, decorato con un attributo chiamato Trigger. Ho commentato il codice del primo stato e le sue transizioni di seguito; gli stati successivi seguono lo stesso schema.

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

Breve e carino, eh!

Questa macchina a stati viene controllata semplicemente inviandole i trigger:

var sm = new Lamp();
sm.PressSwitch(); //go on
sm.PressSwitch(); //go off

sm.PressSwitch(); //go on
sm.GotError();    //get error
sm.PressSwitch(); //go off

Giusto per chiarire, ho aggiunto alcuni commenti al primo stato per aiutarti a capire come usarlo.

    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

        ...

Questo funziona perché il compilatore C # ha effettivamente creato una macchina a stati internamente per ciascun metodo che utilizza yield return. Questo costrutto viene solitamente utilizzato per creare pigramente sequenze di dati, ma in questo caso non siamo realmente interessati alla sequenza restituita (che è comunque nulla di nullo), ma al comportamento dello stato che viene creato sotto il cofano.

La StateMachineclasse base fa alcune riflessioni sulla costruzione per assegnare il codice a ciascuna [Trigger]azione, che imposta il Triggermembro e sposta in avanti la macchina a stati.

Ma non hai davvero bisogno di capire gli interni per poterlo usare.


2
Il "goto" è atroce solo se salta tra i metodi. Ciò, per fortuna, non è consentito in C #.
Brannon,

Buon punto! In effetti, sarei molto impressionato se qualsiasi linguaggio tipicamente statico riuscisse a consentire un gotometodo intermedio.
skrebbel,

3
@Brannon: quale lingua consente gotodi saltare tra i metodi? Non vedo come potrebbe funzionare. No, gotoè problematico perché si traduce in una programmazione procedurale (questo di per sé complica cose carine come il unit test), promuove la ripetizione del codice (notato come InvalidTriggerdeve essere inserito per ogni stato?) E rende infine il flusso del programma più difficile da seguire. Confronta questo con (la maggior parte) altre soluzioni in questo thread e vedrai che questo è l'unico in cui l'intero FSM avviene in un unico metodo. Di solito è abbastanza per destare preoccupazione.
Groo,

1
@Groo, GW-BASIC, per esempio. Aiuta a non avere metodi o funzioni. Oltre a ciò, faccio fatica a capire perché in questo esempio trovi "il flusso del programma più difficile da seguire". È una macchina a stati, "andare a" uno stato da un altro è l'unica cosa che fai. Questo è gotoabbastanza buono.
skrebbel,

3
GW-BASIC consente gotodi passare da una funzione all'altra, ma non supporta le funzioni? :) Hai ragione, l'osservazione "più difficile da seguire" è più un gotoproblema generale , anzi non è un grosso problema in questo caso.
Groo,

13

È possibile codificare un blocco iteratore che consente di eseguire un blocco di codice in modo orchestrato. Il modo in cui il blocco di codice viene suddiviso in realtà non deve corrispondere a nulla, è solo come si desidera codificarlo. Per esempio:

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

In questo caso, quando chiami CountToTen, non viene ancora eseguito nulla. Ciò che ottieni è effettivamente un generatore di macchine a stati, per il quale puoi creare una nuova istanza della macchina a stati. Puoi farlo chiamando GetEnumerator (). Il risultante IEnumerator è effettivamente una macchina a stati che puoi guidare chiamando MoveNext (...).

Quindi, in questo esempio, la prima volta che chiami MoveNext (...) vedrai "1" scritto sulla console, e la prossima volta che chiami MoveNext (...) vedrai 2, 3, 4 e quindi 5, 6, 7 e poi 8 e poi 9, 10. Come puoi vedere, è un meccanismo utile per orchestrare come dovrebbero accadere le cose.


6
Collegamento obbligatorio a un avvertimento equo
vedere

8

Sto postando qui un'altra risposta in quanto si tratta di macchine a stati da una prospettiva diversa; molto visivo.

La mia risposta originale è il classico codice imperativo. Penso che sia abbastanza visivo come il codice va a causa dell'array che rende semplice la visualizzazione della macchina a stati. L'aspetto negativo è che devi scrivere tutto questo. La risposta di Remos allevia lo sforzo di scrivere il codice della piastra della caldaia ma è molto meno visiva. C'è la terza alternativa; disegnando davvero la macchina statale.

Se si utilizza .NET e si può targetizzare la versione 4 del tempo di esecuzione, è possibile utilizzare le attività della macchina a stati del flusso di lavoro . Questi in sostanza ti consentono di disegnare la macchina a stati (proprio come nel diagramma di Giulietta ) e di far eseguire il run-time WF per te.

Vedere l'articolo MSDN Creazione di macchine a stati con Windows Workflow Foundation per maggiori dettagli e questo sito CodePlex per l'ultima versione.

Questa è l'opzione che preferirei sempre quando ho come target .NET perché è facile da vedere, modificare e spiegare ai non programmatori; le immagini valgono più di mille parole come si suol dire!


Penso che la macchina a stati sia una delle parti migliori dell'intera base del flusso di lavoro!
fabsenet,

7

È utile ricordare che le macchine a stati sono un'astrazione e non sono necessari strumenti particolari per crearne una, tuttavia gli strumenti possono essere utili.

Ad esempio è possibile realizzare una macchina a stati con funzioni:

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

Questa macchina cacciava i gabbiani e cercava di colpirli con palloncini d'acqua. Se manca, proverà a sparare fino a quando non colpisce (potrebbe fare con alcune aspettative realistiche;)), altrimenti si gonfierà nella console. Continua a cacciare fino a quando è fuori dai gabbiani per molestare.

Ogni funzione corrisponde a ciascuno stato; gli stati di inizio e fine (o accetta ) non vengono visualizzati. Probabilmente ci sono più stati lì che modellati dalle funzioni però. Ad esempio, dopo aver sparato il pallone, la macchina si trova davvero in un altro stato rispetto a prima, ma ho deciso che questa distinzione non era pratica da fare.

Un modo comune è utilizzare le classi per rappresentare gli stati e quindi collegarli in modi diversi.


7

Ho trovato questo fantastico tutorial online e mi ha aiutato a avvolgere la testa attorno alle macchine a stati finiti.

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

Il tutorial è indipendente dal linguaggio, quindi può essere facilmente adattato alle tue esigenze in C #.

Inoltre, l'esempio usato (una formica in cerca di cibo) è facile da capire.


Dal tutorial:

inserisci qui la descrizione dell'immagine

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

    (...)
}

1
Sebbene questo collegamento possa rispondere alla domanda, è meglio includere qui le parti essenziali della risposta e fornire il collegamento come riferimento. Le risposte di solo collegamento possono diventare non valide se la pagina collegata cambia. - Dalla recensione
drneel,

@drneel Potrei copiare e incollare bit dal tutorial ... ma non toglierebbe credito all'autore?
Jet Blue,

1
@JetBlue: lascia il link nella risposta come riferimento e includi i bit pertinenti nelle tue parole nel post della risposta in modo da non violare il copyright di nessuno. So che sembra rigoroso, ma molte risposte sono diventate molto, molto migliori grazie a questa regola.
Flimm,

6

Oggi approfondisco il modello di progettazione statale. Ho fatto e testato ThreadState, che equivale (+/-) al threading in C #, come descritto nell'immagine da Threading in C #

inserisci qui la descrizione dell'immagine

È possibile aggiungere facilmente nuovi stati, configurare i passaggi da uno stato all'altro è molto semplice perché incapsulato nell'implementazione dello stato

Implementazione e utilizzo su: Implementa .NET ThreadState di State Design Pattern


2
Il link è morto. Ne hai un altro?
lancia il

5

Non ho ancora provato a implementare un FSM in C #, ma tutto ciò suona (o sembra) molto complicato al modo in cui ho gestito i FSM in passato in linguaggi di basso livello come C o ASM.

Credo che il metodo che ho sempre conosciuto si chiama qualcosa di simile a un "ciclo iterativo". In esso, essenzialmente hai un ciclo 'while' che esce periodicamente in base agli eventi (interruzioni), quindi ritorna di nuovo al ciclo principale.

All'interno dei gestori di interrupt, si passa a CurrentState e si restituisce NextState, che quindi sovrascrive la variabile CurrentState nel ciclo principale. Lo fai all'infinito fino alla chiusura del programma (o al reset del microcontrollore).

Ciò che vedo in altre risposte sembra molto complicato rispetto a come un FSM è, secondo me, destinato a essere implementato; la sua bellezza sta nella sua semplicità e FSM può essere molto complicato con molti, molti stati e transizioni, ma consentono di complicare e digerire processi complicati.

Mi rendo conto che la mia risposta non dovrebbe includere un'altra domanda, ma sono costretto a chiedere: perché queste altre soluzioni proposte sembrano essere così complicate?
Sembrano simili a colpire un chiodo con una mazza gigante.


1
Sono completamente d'accordo. Un semplice ciclo while con un'istruzione switch è il più semplice possibile.
lancia il

2
A meno che tu non abbia una macchina a stati molto complicata con molti stati e condizioni, in cui finiresti con più switch nidificati. Inoltre, potrebbe esserci una penalità nell'attesa di occupato, a seconda dell'implementazione del ciclo.
Sune Rievers,

3

Che incontro StatePattern. Si adatta alle tue esigenze?

Penso che sia relativo al contesto, ma sicuramente vale la pena provare.

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

Ciò consente ai tuoi stati di decidere dove andare e non la classe "oggetto".

Bruno


1
Il modello di stato si occupa di una classe che può agire in modo diverso in base allo stato / modalità in cui si trova, non si occupa della transizione tra stati.
Eli Algranti,

3

A mio avviso, una macchina a stati non è pensata solo per cambiare stato ma anche (molto importante) per gestire trigger / eventi all'interno di uno stato specifico. Se si desidera comprendere meglio il modello di progettazione della macchina a stati, è possibile trovare una buona descrizione nel libro Head First Design Patterns, pagina 320 .

Non si tratta solo degli stati all'interno delle variabili, ma anche della gestione dei trigger all'interno dei diversi stati. Ottimo capitolo (e no, non ho alcun costo per menzionare questo :-) che contiene solo una spiegazione facile da capire.


3

Ho appena contribuito con questo:

https://code.google.com/p/ysharp/source/browse/#svn%2Ftrunk%2FStateMachinesPoC

Ecco uno degli esempi che dimostrano l'invio diretto e indiretto di comandi, con stati come IObserver (del segnale), quindi rispondenti a una sorgente di segnale, IObservable (del segnale):

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: questo esempio è piuttosto artificiale e principalmente inteso a dimostrare una serie di caratteristiche ortogonali. Raramente dovrebbe esserci una reale necessità di implementare il dominio del valore dello stato stesso da parte di una classe in piena regola, usando il CRTP (vedi: http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern ) come questo.

Ecco un caso d'uso di implementazione sicuramente più semplice e probabilmente molto più comune (utilizzando un tipo enum semplice come dominio dei valori degli stati), per la stessa macchina a stati e con lo stesso caso di test:

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


Non è strano che ogni istanza di stato abbia la propria copia del grafico di stato?
Groo,

@Groo: no, non lo fanno. Solo le istanze di Television costruite usando il costruttore privato con una stringa nulla per il moniker (quindi, chiamando il metodo 'Build' protetto) avranno un grafico di stato, come macchine a stati. Gli altri, chiamati istanze di Television (con un moniker non nullo per quello scopo convenzionale e ad hoc) saranno semplici stati "fissi" (per così dire), che fungeranno da costanti di stato (che il / i grafico / i di stato di le macchine a stato reale faranno riferimento ai loro vertici). 'HTH,
YSharp,

Ok ho capito. Ad ogni modo, IMHO, sarebbe stato meglio se avessi incluso del codice che gestisce effettivamente queste transizioni. In questo modo, serve solo come esempio dell'utilizzo di un'interfaccia (IMHO) non così ovvia per la tua libreria. Ad esempio, come viene StateChangerisolto? Attraverso la riflessione? È davvero necessario?
Groo,

1
@Groo: buona osservazione. Non è in effetti necessario riflettere sul gestore in quel primo esempio perché è fatto programmaticamente lì esattamente e può essere associato staticamente / tipo controllato (diversamente da quando tramite attributi personalizzati). Quindi anche questo lavoro come previsto: 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
Grazie per i tuoi sforzi!
Groo,

3

Ho realizzato questa macchina a stati generici dal codice di Giulietta. Funziona benissimo per me.

Questi sono i vantaggi:

  • puoi creare una nuova macchina a stati in codice con due enumerazioni TStateeTCommand ,
  • aggiunto struct TransitionResult<TState>per avere un maggiore controllo sui risultati di output di[Try]GetNext() metodi
  • esponendo classe annidata StateTransition solo attraverso AddTransition(TState, TCommand, TState)rendendo più facile il lavoro con esso

Codice:

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

Questo è il tipo di ritorno del metodo TryGetNext:

public struct TransitionResult<TState>
{
    public TransitionResult(TState newState, bool isValid)
    {
        NewState = newState;
        IsValid = isValid;
    }
    public TState NewState;
    public bool IsValid;
}

Come usare:

Ecco come è possibile creare un OnlineDiscountStateMachine dalla classe generica:

Definisci un enum OnlineDiscountStateper i suoi stati e un enumOnlineDiscountCommand per i suoi comandi.

Definisci una classe OnlineDiscountStateMachine derivata dalla classe generica usando quei due enumeratori

Deriva il costruttore da in base(OnlineDiscountState.InitialState)modo che lo stato iniziale sia impostato suOnlineDiscountState.InitialState

Utilizzare AddTransitiontutte le volte che è necessario

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

usa la macchina a stati derivati

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

1

Penso che la macchina a stati proposta da Juliet abbia un errore: il metodo GetHashCode può restituire lo stesso codice hash per due diverse transizioni, ad esempio:

Stato = Attivo (1), Comando = Pausa (2) => HashCode = 17 + 31 + 62 = 110

Stato = In pausa (2), Comando = Fine (1) => HashCode = 17 + 62 + 31 = 110

Per evitare questo errore, il metodo dovrebbe essere così:

public override int GetHashCode()
   {
            return 17 + 23 * CurrentState.GetHashCode() + 31 * Command.GetHashCode();
   }

alex


1
Il codice hash non è necessario per restituire un numero univoco per ogni possibile combinazione, solo un valore distinto con una buona distribuzione nell'intervallo di destinazione (in questo caso l'intervallo è tutti i possibili intvalori). Ecco perché HashCodeviene sempre implementato insieme a Equals. Se i codici hash sono gli stessi, gli oggetti vengono controllati per l'esattezza dell'equlità usando il Equalsmetodo.
Dmitry Avtonomov il

0

FiniteStateMachine è una macchina a stato semplice, scritta in C # Link

I vantaggi che usi la mia libreria FiniteStateMachine:

  1. Definire una classe di "contesto" per presentare una singola interfaccia al mondo esterno.
  2. Definire una classe base astratta dello stato.
  3. Rappresenta i diversi "stati" della macchina a stati come classi derivate della classe base a stati.
  4. Definire il comportamento specifico dello stato nelle classi derivate dallo stato appropriate.
  5. Mantenere un puntatore allo "stato" corrente nella classe "contesto".
  6. Per modificare lo stato della macchina a stati, modificare il puntatore "stato" corrente.

Scarica DLL Download

Esempio su 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;


     }

 }

1
Ha la licenza GNU GPL.
Der_Meister

0

Consiglierei state.cs . Personalmente ho usato state.js (la versione JavaScript) e ne sono molto contento. Quella versione C # funziona in modo simile.

Si istanzia stati:

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

Hai un'istanza di alcune transizioni:

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

Definisci azioni su stati e transizioni:

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

E questo è (praticamente). Guarda il sito web per maggiori informazioni.



0

Un'altra alternativa in questo repository https://github.com/lingkodsoft/StateBliss ha usato una sintassi fluida, supporta i trigger.

    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
    }
}

0

Puoi usare la mia soluzione, questo è il modo più conveniente. È anche gratuito.

Crea una macchina a stati in tre passaggi:

1. Creare uno schema nell'editor dei nodi🔗 e caricarlo nel progetto usando la libreria📚

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

2. Descrivi la logica della tua app sugli eventi⚡

stateMachine.GetState ( "State1") Su uscita (Action1).;
stateMachine.GetState ( "Stato2") OnEntry (Azione2).;
stateMachine.GetTransition ( "Transition1") OnInvoke (Action3).;
stateMachine.OnChangeState (Action4);

3. Eseguire la macchina a stati🚘

stateMachine.Start ();

link:

Editor dei nodi: https://github.com/SimpleStateMachine/SimpleStateMachineNodeEditor

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.