Exemple de machine d'état simple en C #?


259

Mettre à jour:

Encore merci pour les exemples, ils ont été très utiles et avec ce qui suit, je ne veux rien leur enlever.

Les exemples actuellement fournis, pour autant que je les comprenne et les machines à états, ne sont-ils que la moitié de ce que nous comprenons habituellement par une machine à états?
Dans le sens où les exemples changent d'état, mais cela n'est représenté qu'en changeant la valeur d'une variable (et en permettant différents changements de valeur dans différents états), alors qu'en général une machine à états devrait également changer son comportement, et le comportement non (seulement) dans le sens d'autoriser différents changements de valeur pour une variable en fonction de l'état, mais dans le sens d'autoriser l'exécution de différentes méthodes pour différents états.

Ou ai-je une idée fausse des machines d'état et de leur utilisation courante?

Meilleures salutations


Question d'origine:

J'ai trouvé cette discussion sur les machines d'état et les blocs d'itérateur en c # et les outils pour créer des machines d'état et quoi pas pour C #, donc j'ai trouvé beaucoup de choses abstraites mais en tant que noob tout cela est un peu déroutant.

Il serait donc formidable que quelqu'un puisse fournir un exemple de code source C # qui réalise une machine à états simple avec peut-être 3,4 états, juste pour avoir l'essentiel.



Vous vous posez des questions sur les machines à états en général ou simplement celles basées sur des itérateurs?
Skurmedel

2
Il y a .Net Core Stateless lib avec des exemples, des daigrammes DAG etc. - mérite d'être examiné: hanselman.com/blog/…
zmische

Réponses:


417

Commençons par ce diagramme d'état simple:

diagramme de machine à état simple

Nous avons:

  • 4 états (inactif, actif, en pause et quitté)
  • 5 types de transitions d'état (commande de début, commande de fin, commande de pause, commande de reprise, commande de sortie).

Vous pouvez le convertir en C # de plusieurs manières, par exemple en exécutant une instruction switch sur l'état et la commande en cours, ou en recherchant des transitions dans une table de transition. Pour cette machine à états simple, je préfère une table de transition, qui est très facile à représenter à l'aide d'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();
        }
    }
}

Par préférence personnelle, j'aime concevoir mes machines à états avec une GetNextfonction pour renvoyer le prochain état de façon déterministe , et une MoveNextfonction pour muter la machine à états.


66
+1 pour l'implémentation correcte de l' GetHashCode()utilisation des nombres premiers.
ja72

13
Pourriez-vous s'il vous plaît m'expliquer le but de GetHashCode ()?
Siddharth

14
@Siddharth: La StateTransitionclasse est utilisée comme clé dans le dictionnaire et l'égalité des clés est importante. Deux instances distinctes de StateTransitiondoivent être considérées comme égales tant qu'elles représentent la même transition (par exemple CurrentStateet Commandsont les mêmes). Pour mettre en œuvre l' égalité , vous devez remplacer Equalsainsi que GetHashCode. En particulier, le dictionnaire utilisera le code de hachage et deux objets égaux doivent retourner le même code de hachage. Vous obtenez également de bonnes performances si pas trop d'objets non égaux partagent le même code de hachage, c'est pourquoi il GetHashCodeest implémenté comme indiqué.
Martin Liversage

14
Bien que cela vous donne sûrement une machine à états (et une implémentation ish C # appropriée également), je pense qu'il manque toujours la réponse à la question du PO sur le changement de comportement? Après tout, il calcule simplement les états mais le comportement lié aux changements d'état, la viande réelle du programme et généralement appelée événements d'entrée / sortie, est toujours manquant.
stijn

2
Si quelqu'un en a besoin: j'ai ajusté cette machine et je l'ai utilisée dans mon jeu d'unité. Il est disponible sur git hub: github.com/MarcoMig/Finite-State-Machine-FSM
Max_Power89

73

Vous souhaiterez peut-être utiliser l'une des machines à états finis open source existantes. Par exemple bbv.Common.StateMachine trouvé sur http://code.google.com/p/bbvcommon/wiki/StateMachine . Il a une syntaxe fluide très intuitive et de nombreuses fonctionnalités telles que, les actions d'entrée / sortie, les actions de transition, les gardes, la mise en œuvre hiérarchique, passive (exécutée sur le thread de l'appelant) et la mise en œuvre active (propre thread sur lequel le fsm s'exécute, les événements sont ajoutés à une file d'attente).

En prenant l'exemple de Juliette, la définition de la machine d'état devient très simple:

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

Mise à jour : l'emplacement du projet a été déplacé vers: https://github.com/appccelerate/statemachine


4
Merci d'avoir référencé cette excellente machine d'état open source. Puis-je demander comment obtenir l'état actuel?
Ramazan Polat

3
Vous ne pouvez pas et vous ne devriez pas. L'État est quelque chose d'instable. Lorsque vous demandez l'état, il est possible que vous soyez au milieu d'une transition. Toutes les actions doivent être effectuées dans les transitions, les entrées et sorties d'état. Si vous voulez vraiment avoir l'état, vous pouvez ajouter un champ local et affecter l'état dans une action d'entrée.
Remo Gloor

4
La question est de savoir de quoi "vous avez besoin" et si vous avez vraiment besoin de l'état SM ou d'un autre type d'état. Par exemple, si vous avez besoin d'un texte d'affichage, plusieurs énoncés peuvent avoir le même texte d'affichage, par exemple si la préparation de l'envoi a plusieurs sous-états. Dans ce cas, vous devez faire exactement ce que vous avez l'intention de faire. Mettez à jour du texte d'affichage aux bons endroits. Par exemple, dans ExecuteOnEntry. Si vous avez besoin de plus d'informations, posez une nouvelle question et énoncez exactement votre problème car cela sort du sujet ici.
Remo Gloor du

Ok, je pose une nouvelle question et j'attends que vous répondiez. Parce que je ne pense pas que quelqu'un d'autre résout ce problème puisque vous avez la meilleure réponse mais que le questionneur n'a pas accepté. Je posterai l'url de la question ici. Merci.
Ramazan Polat

4
+1 pour l'API fluide et déclarative. C'est génial. BTW, le code google semble obsolète. Leur nouveau site de projet est sur GitHub ici
KFL

52

Voici un exemple d'une machine à états finis très classique, modélisant un appareil électronique très simplifié (comme un téléviseur)

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
pour quiconque découvre les machines à états, c'est un excellent premier exemple pour se mouiller les pieds en premier.
PositiveGuy

2
Je suis nouveau dans les machines d'état et sérieusement, cela m'a apporté La Lumière - merci!
MC5

1
J'ai aimé cette implémentation. Pour tous ceux qui pourraient trébucher sur ce point, une légère "amélioration". Dans la classe FSM, j'ai ajouté private void DoNothing() {return;}et remplacé toutes les instances de null par this.DoNothing. A l'effet secondaire agréable de retourner l'état actuel.
Sethmo011

1
Je me demande s'il y a un raisonnement derrière certains de ces noms. Quand je regarde cela, ma première intuition est de renommer les éléments du Statesto Unpowered, Standby, On. Mon raisonnement est que si quelqu'un me demandait dans quel état se trouve ma télévision, je dirais «Off» et non «Start». J'ai aussi changé StandbyWhenOnet StandbyWhenOffvers TurnOnet TurnOff. Cela rend le code plus intuitif, mais je me demande s'il existe des conventions ou d'autres facteurs qui rendent ma terminologie moins appropriée.
Jason Hamje

Semble raisonnable, je ne suivais vraiment aucune convention de dénomination d'état; nom qui a du sens pour tout ce que vous modélisez.
Pete Stensønes

20

Une auto-promo sans vergogne ici, mais il y a quelque temps, j'ai créé une bibliothèque appelée YieldMachine qui permet de décrire une machine à états à complexité limitée d'une manière très propre et simple. Par exemple, considérons une lampe:

machine d'état d'une lampe

Notez que cette machine à états a 2 déclencheurs et 3 états. Dans le code YieldMachine, nous écrivons une seule méthode pour tous les comportements liés à l'état, dans laquelle nous commettons l'horrible atrocité de l'utilisation gotopour chaque état. Un déclencheur devient une propriété ou un champ de type Action, décoré d'un attribut appelé Trigger. J'ai commenté le code du premier état et ses transitions ci-dessous; les états suivants suivent le même schéma.

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

Bref et sympa, hein!

Cette machine d'état est contrôlée simplement en lui envoyant des déclencheurs:

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

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

Juste pour clarifier, j'ai ajouté quelques commentaires au premier état pour vous aider à comprendre comment l'utiliser.

    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

        ...

Cela fonctionne car le compilateur C # a réellement créé une machine d'état en interne pour chaque méthode qui utilise yield return. Cette construction est généralement utilisée pour créer des séquences de données paresseusement, mais dans ce cas, nous ne sommes pas réellement intéressés par la séquence retournée (qui est de toute façon nulle), mais par le comportement d'état qui est créé sous le capot.

La StateMachineclasse de base réfléchit à la construction pour attribuer du code à chaque [Trigger]action, ce qui définit le Triggermembre et fait avancer la machine d'état.

Mais vous n'avez pas vraiment besoin de comprendre les internes pour pouvoir l'utiliser.


2
Le "goto" n'est atroce que s'il passe d'une méthode à l'autre. Heureusement, cela n'est pas autorisé en C #.
Brannon

Bon point! En fait, je serais très impressionné si un langage typé statistiquement réussissait à autoriser une gotométhode entre les deux.
skrebbel

3
@Brannon: quelle langue permet gotode passer d'une méthode à l'autre? Je ne vois pas comment cela pourrait fonctionner. Non, gotoc'est problématique car cela entraîne une programmation procédurale (cela complique en soi de belles choses comme les tests unitaires), favorise la répétition du code (vous avez remarqué comment InvalidTriggerdoit être inséré pour chaque état?) Et rend finalement le flux du programme plus difficile à suivre. Comparez cela à (la plupart) des autres solutions de ce fil et vous verrez que c'est la seule où le FSM entier se produit dans une seule méthode. C'est généralement suffisant pour soulever une préoccupation.
Groo

1
@Groo, GW-BASIC, par exemple. Cela aide qu'il n'a pas de méthodes, ni même de fonctions. En plus de cela, j'ai beaucoup de mal à comprendre pourquoi vous trouvez le "flux de programme plus difficile à suivre" dans cet exemple. C'est une machine à états, "aller à" un autre état est la seule chose que vous faites. Cela correspond gotoassez bien.
skrebbel

3
GW-BASIC permet gotode sauter entre les fonctions, mais il ne prend pas en charge les fonctions? :) Vous avez raison, la remarque "plus difficile à suivre" est plus un gotoproblème général , en fait pas vraiment un problème dans ce cas.
Groo

13

Vous pouvez coder un bloc d'itérateur qui vous permet d'exécuter un bloc de code de manière orchestrée. La façon dont le bloc de code est divisé ne doit vraiment correspondre à rien, c'est simplement la façon dont vous voulez le coder. Par exemple:

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

Dans ce cas, lorsque vous appelez CountToTen, rien ne s'exécute réellement, pour l'instant. Ce que vous obtenez est en fait un générateur de machine d'état, pour lequel vous pouvez créer une nouvelle instance de la machine d'état. Pour ce faire, appelez GetEnumerator (). L'IEnumerator résultant est en fait une machine d'état que vous pouvez piloter en appelant MoveNext (...).

Ainsi, dans cet exemple, la première fois que vous appelez MoveNext (...), vous verrez "1" écrit sur la console, et la prochaine fois que vous appelez MoveNext (...), vous verrez 2, 3, 4 et puis 5, 6, 7 puis 8, puis 9, 10. Comme vous pouvez le voir, c'est un mécanisme utile pour orchestrer comment les choses doivent se produire.


6
Lien obligatoire avertissement équitable
sehe

8

Je poste une autre réponse ici car il s'agit de machines à états sous un angle différent; très visuel.

Ma réponse originale est un code impératif classique. Je pense que son aspect est assez visuel à cause du tableau qui rend la visualisation de la machine à états simple. L'inconvénient est que vous devez écrire tout cela. La réponse de Remos allège l'effort d'écriture du code de la plaque de la chaudière mais est beaucoup moins visuelle. Il y a la troisième alternative; vraiment dessiner la machine d'état.

Si vous utilisez .NET et pouvez cibler la version 4 de l'exécution, vous avez la possibilité d'utiliser les activités de la machine d'état du workflow . Celles-ci vous permettent essentiellement de dessiner la machine à états (un peu comme dans le diagramme de Juliette ) et de le faire exécuter par WF au moment de l'exécution.

Voir l'article MSDN Building State Machines with Windows Workflow Foundation pour plus de détails, et ce site CodePlex pour la dernière version.

C'est l'option que je préférerais toujours lors du ciblage de .NET car il est facile à voir, à modifier et à expliquer aux non-programmeurs; les images valent mille mots comme on dit!


Je pense que la machine d'état est l'une des meilleures parties de toute la fondation du workflow!
fabsenet

7

Il est utile de se rappeler que les machines à états sont une abstraction et que vous n'avez pas besoin d'outils particuliers pour en créer un, mais les outils peuvent être utiles.

Vous pouvez par exemple réaliser une machine à états avec des fonctions:

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

Cette machine chassait les goélands et tentait de les frapper avec des ballons d'eau. S'il manque, il essaiera d'en tirer un jusqu'à ce qu'il frappe (ce qui pourrait faire avec des attentes réalistes;)), sinon il se réjouira dans la console. Il continue de chasser jusqu'à ce qu'il soit à court de goélands pour harceler.

Chaque fonction correspond à chaque état; les états de début et de fin (ou d' acceptation ) ne sont pas affichés. Cependant, il y a probablement plus d'états que modélisés par les fonctions. Par exemple, après avoir tiré le ballon, la machine est vraiment dans un autre état qu'elle ne l'était avant, mais j'ai décidé que cette distinction n'était pas pratique à faire.

Une manière courante consiste à utiliser des classes pour représenter des états, puis à les connecter de différentes manières.


7

J'ai trouvé ce super didacticiel en ligne et il m'a aidé à comprendre les machines à états finis.

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

Le tutoriel est indépendant du langage, il peut donc facilement être adapté à vos besoins en C #.

De plus, l'exemple utilisé (une fourmi à la recherche de nourriture) est facile à comprendre.


Du tutoriel:

entrez la description de l'image ici

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
Bien que ce lien puisse répondre à la question, il est préférable d'inclure les parties essentielles de la réponse ici et de fournir le lien de référence. Les réponses de lien uniquement peuvent devenir invalides si la page liée change. - De l'avis
drneel

@drneel Je pourrais copier et coller des morceaux du tutoriel ... mais cela ne priverait-il pas l'auteur de son mérite?
Jet Blue

1
@JetBlue: Laissez le lien dans la réponse comme référence et incluez les bits pertinents dans vos propres mots dans le message de réponse afin de ne pas violer les droits d'auteur de quiconque. Je sais que cela semble strict, mais de nombreuses réponses sont devenues beaucoup, beaucoup mieux grâce à cette règle.
Flimm

6

Aujourd'hui, je suis profondément dans le modèle de conception d'état. J'ai fait et testé ThreadState, qui équivaut à (+/-) à Threading en C #, comme décrit dans l'image de Threading en C #

entrez la description de l'image ici

Vous pouvez facilement ajouter de nouveaux états, configurer les mouvements d'un état à l'autre est très facile car il est encapsulé dans la mise en œuvre de l'état

Implémentation et utilisation à: Implements .NET ThreadState by State Design Pattern


2
Le lien est mort. Avez-vous un autre?
roule

5

Je n'ai pas encore essayé d'implémenter un FSM en C #, mais tout cela semble (ou semble) très compliqué à la façon dont je gérais les FSM dans le passé dans des langages de bas niveau comme C ou ASM.

Je crois que la méthode que j'ai toujours connue s'appelle quelque chose comme une "boucle itérative". Vous y trouverez essentiellement une boucle «while» qui se termine périodiquement en fonction des événements (interruptions), puis revient à nouveau à la boucle principale.

Dans les gestionnaires d'interruption, vous passeriez un CurrentState et renverriez un NextState, qui écraserait ensuite la variable CurrentState dans la boucle principale. Vous faites cela à l'infini jusqu'à la fermeture du programme (ou la réinitialisation du microcontrôleur).

Ce que je vois d'autres réponses semblent toutes très compliquées par rapport à la façon dont un FSM est, dans mon esprit, destiné à être mis en œuvre; sa beauté réside dans sa simplicité et FSM peut être très compliqué avec de nombreux états et transitions, mais ils permettent de décomposer et de digérer facilement des processus compliqués.

Je réalise que ma réponse ne devrait pas inclure une autre question, mais je suis obligé de demander: pourquoi ces autres solutions proposées semblent-elles si compliquées?
Ils semblent s'apparenter à frapper un petit clou avec un marteau géant.


1
Entièrement d'accord. Une simple boucle while avec une instruction switch est aussi simple que possible.
roule

2
Sauf si vous avez une machine d'état très compliquée avec de nombreux états et conditions, où vous vous retrouveriez avec plusieurs commutateurs imbriqués. En outre, il peut y avoir une pénalité dans l'attente occupée, selon votre implémentation de boucle.
Sune Rievers

3

Quel combat StatePattern. Cela correspond-il à vos besoins?

Je pense que son contexte est lié, mais ça vaut le coup à coup sûr.

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

Cela permet à vos états de décider où aller et non à la classe "objet".

Bruno


1
Le modèle d'état traite d'une classe qui peut agir différemment en fonction de l'état / du mode dans lequel il se trouve, il ne traite pas de la transition entre les états.
Eli Algranti

3

À mon avis, une machine à états n'est pas seulement destinée à changer d'états, mais aussi (très importante) à gérer les déclencheurs / événements dans un état spécifique. Si vous voulez mieux comprendre le modèle de conception de la machine à états, une bonne description peut être trouvée dans le livre Modèles de conception en tête, page 320 .

Il ne s'agit pas seulement des états dans les variables mais aussi de la gestion des déclencheurs dans les différents états. Grand chapitre (et non, il n'y a pas de frais pour moi en mentionnant ceci :-) qui contient juste une explication facile à comprendre.


3

Je viens de contribuer ceci:

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

Voici l'un des exemples de démonstration de l'envoi direct et indirect de commandes, avec des états comme IObserver (de signal), répondant ainsi à une source de signal, IObservable (de signal):

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

Remarque: cet exemple est plutôt artificiel et vise principalement à démontrer un certain nombre de caractéristiques orthogonales. Il devrait rarement y avoir un réel besoin d'implémenter le domaine de valeurs d'état lui-même par une classe complète, en utilisant le CRTP (voir: http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern ) comme ceci.

Voici un cas d'utilisation d'implémentation certainement plus simple et probablement beaucoup plus courant (en utilisant un type d'énumération simple comme domaine de valeur d'états), pour la même machine d'état et avec le même cas de 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


N'est-il pas un peu étrange que chaque instance d'état ait sa propre copie du graphe d'état?
Groo

@Groo: non, ils ne le font pas. Seules les instances de Television construites à l'aide du constructeur privé avec une chaîne nulle pour le surnom (d'où l'appel de la méthode protégée 'Build') auront un graphe d'état, comme machines d'état. Les autres, nommés instances de Télévision (avec un surnom non nul à cette fin conventionnelle et ad hoc) seront de simples états "fixes" (pour ainsi dire), servant de constantes d'état (que le (s) graphe (s) d'état de les machines à états réelles feront référence à leurs sommets). 'HTH,
YSharp

D'accord, je comprends. Quoi qu'il en soit, à mon humble avis, il aurait été préférable que vous incluiez du code qui gère réellement ces transitions. De cette façon, il ne sert que d'exemple d'utilisation d'une interface (à mon humble avis) pas si évidente pour votre bibliothèque. Par exemple, comment est StateChangerésolu? Par la réflexion? Est-ce vraiment nécessaire?
Groo

1
@Groo: Bonne remarque. Il n'est pas nécessaire en effet de réfléchir sur le gestionnaire dans ce premier exemple, car il est fait par programme là-dedans avec précision et peut être lié statiquement / vérifié (contrairement à quand via des attributs personnalisés). Donc, ce travail comme prévu aussi: 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
Merci pour votre effort!
Groo

3

J'ai fait cette machine à états générique à partir du code de Juliette. Cela fonctionne très bien pour moi.

Ce sont les avantages:

  • vous pouvez créer une nouvelle machine d'état en code avec deux énumérations TStateet TCommand,
  • structure ajoutée TransitionResult<TState>pour avoir plus de contrôle sur les résultats de sortie des [Try]GetNext()méthodes
  • exposer classe imbriquée StateTransition seulement à travers le AddTransition(TState, TCommand, TState)rendant plus facile de travailler avec elle

Code:

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

Il s'agit du type de retour de la méthode TryGetNext:

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

Comment utiliser:

Voici comment créer un à OnlineDiscountStateMachinepartir de la classe générique:

Définissez une énumération OnlineDiscountStatepour ses états et une énumération OnlineDiscountCommandpour ses commandes.

Définir une classe OnlineDiscountStateMachine dérivée de la classe générique à l'aide de ces deux énumérations

Dérivez le constructeur de base(OnlineDiscountState.InitialState)sorte que l' état initial soit défini surOnlineDiscountState.InitialState

Utilisez AddTransitionautant de fois que nécessaire

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

utiliser la machine à états dérivée

    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

Je pense que la machine d'état proposée par Juliet a une erreur: la méthode GetHashCode peut renvoyer le même code de hachage pour deux transitions différentes, par exemple:

État = Actif (1), Commande = Pause (2) => HashCode = 17 + 31 + 62 = 110

État = En pause (2), Commande = Fin (1) => HashCode = 17 + 62 + 31 = 110

Pour éviter cette erreur, la méthode doit être comme ceci:

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

Alex


1
Le code de hachage n'est pas requis pour renvoyer un nombre unique pour toute combinaison possible, seulement une valeur distincte avec une bonne distribution sur la plage cible (dans ce cas, la plage est toutes les intvaleurs possibles ). C'est pourquoi HashCodeest toujours mis en œuvre avec Equals. Si les codes de hachage sont les mêmes, alors les objets sont vérifiés pour une équation exacte en utilisant la Equalsméthode.
Dmitry Avtonomov

0

FiniteStateMachine est une machine d'état simple, écrite en C # Link

Avantages d'utiliser ma bibliothèque FiniteStateMachine:

  1. Définissez une classe "contextuelle" pour présenter une interface unique au monde extérieur.
  2. Définissez une classe de base abstraite d'État.
  3. Représentez les différents "états" de la machine à états en tant que classes dérivées de la classe de base State.
  4. Définissez le comportement spécifique à l'état dans les classes dérivées d'État appropriées.
  5. Maintenez un pointeur sur l'état actuel dans la classe "context".
  6. Pour modifier l'état de la machine d'état, modifiez le pointeur "état" actuel.

Télécharger DLL Télécharger

Exemple sur 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
Il a une licence GNU GPL.
Der_Meister

0

Je recommanderais state.cs . J'ai personnellement utilisé state.js (la version JavaScript) et j'en suis très content. Cette version C # fonctionne de manière similaire.

Vous instanciez des états:

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

Vous instanciez certaines transitions:

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

Vous définissez des actions sur les états et les transitions:

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

Et c'est à peu près tout. Consultez le site Web pour plus d'informations.



0

Une autre alternative dans ce dépôt https://github.com/lingkodsoft/StateBliss utilise une syntaxe fluide, prend en charge les déclencheurs.

    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

Vous pouvez utiliser ma solution, c'est le moyen le plus pratique. C'est aussi gratuit.

Créez une machine à états en trois étapes:

1. Créez un schéma dans l' éditeur de nœuds🔗 et chargez-le dans votre projet à l'aide de la bibliothèque📚

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

2. Décrivez la logique de votre application sur les événements⚡

stateMachine.GetState ("State1"). OnExit (Action1);
stateMachine.GetState ("State2"). OnEntry (Action2);
stateMachine.GetTransition ("Transition1"). OnInvoke (Action3);
stateMachine.OnChangeState (Action4);

3. Exécutez la machine d'état🚘

stateMachine.Start ();

Liens:

Éditeur de nœuds: https://github.com/SimpleStateMachine/SimpleStateMachineNodeEditor

Bibliothèque: https://github.com/SimpleStateMachine/SimpleStateMachineLibrary

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.