293 votes

Exemple simple de machine à états en C# ?

Mise à jour :

Encore une fois, merci pour les exemples, ils ont été très utiles et, avec ce qui suit, je ne veux pas leur enlever quoi que ce soit. de leur enlever quoi que ce soit.

Les exemples actuellement donnés, pour autant que je les comprenne & les machines à état, ne sont-ils pas seulement la moitié de ce que nous comprenons habituellement par une machine à état ?
Dans le sens où les exemples changent d'état, mais uniquement en changeant la valeur d'une variable (et en autorisant des changements de valeur différents selon les états), alors qu'habituellement, un automate à états devrait également changer de comportement, et ce comportement non pas (uniquement) dans le sens où il autorise des changements de valeur différents pour une variable selon l'état, mais dans le sens où il autorise l'exécution de méthodes différentes selon les états.

Ou bien ai-je une idée fausse des machines à états et de leur utilisation courante ?

Meilleures salutations


Question originale :

J'ai trouvé cette discussion sur machines à états et blocs itérateurs en c# et des outils pour créer des machines à états et autres pour C#, j'ai donc trouvé beaucoup de choses abstraites, mais en tant que novice, tout cela est un peu confus.

Ce serait donc formidable si quelqu'un pouvait fournir un exemple de code source C# qui réalise une machine à états simple avec peut-être 3 ou 4 états, juste pour comprendre l'essentiel.


0 votes

Vous vous interrogez sur les machines à états en général ou seulement sur celles basées sur des itérateurs ?

2 votes

Il existe une librairie Stateless .Net Core avec des exemples, des DAGs, des daigrammes, etc. - Cela vaut la peine de l'examiner : hanselman.com/blog/

461voto

Juliet Points 40758

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

simple state machine diagram

Nous avons :

  • 4 états (Inactif, Actif, Pause, et Exit)
  • 5 types de transitions d'état (commande de début, commande de fin, commande de pause, commande de reprise, commande de sortie).

Vous pouvez convertir cela en C# de plusieurs façons, par exemple en exécutant une instruction de commutation sur l'état actuel et la commande, ou en recherchant les 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'une instruction 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 fonction GetNext pour retourner l'état suivant de manière déterministe et un MoveNext pour muter la machine à états.

68 votes

+1 pour la mise en œuvre correcte de GetHashCode() en utilisant des primes.

15 votes

Pourriez-vous m'expliquer le but de GetHashCode() ?

16 votes

@Siddharth : Le StateTransition est utilisée comme clé dans le dictionnaire et l'égalité des clés est importante. Deux instances distinctes de StateTransition doivent être considérés comme égaux tant qu'ils représentent la même transition (par ex. CurrentState et Command sont identiques). Pour implémenter l'égalité, vous devez surcharger Equals ainsi que GetHashCode . En particulier, le dictionnaire utilise le code de hachage et deux objets égaux doivent renvoyer le même code de hachage. On obtient également de bonnes performances si un nombre limité d'objets non égaux partagent le même code de hachage. GetHashCode est mis en œuvre comme indiqué.

81voto

Remo Gloor Points 26195

Vous pouvez utiliser l'une des machines à états finis open source existantes. Par exemple, bbv.Common.StateMachine à l'adresse suivante http://code.google.com/p/bbvcommon/wiki/StateMachine . Il possède une syntaxe fluide très intuitive et de nombreuses fonctionnalités telles que des actions d'entrée/sortie, des actions de transition, des gardes, une implémentation hiérarchique, passive (exécutée sur le thread de l'appelant) et 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 à états devient très facile :

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 : Le lieu du projet a été déplacé à : https://github.com/appccelerate/statemachine

5 votes

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

4 votes

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 le cadre de transitions, d'entrées et de sorties d'état. Si vous voulez vraiment avoir l'état, vous pouvez ajouter un champ local et assigner l'état dans une action d'entrée.

4 votes

La question est de savoir pourquoi vous en 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 états pourraient avoir le même texte d'affichage, par exemple si la préparation à l'envoi a plusieurs sous-états. Dans ce cas, vous devez faire exactement ce que vous avez l'intention de faire. Mettez à jour le texte d'affichage aux bons endroits. Par exemple, dans ExecuteOnEntry. Si vous avez besoin de plus d'informations, posez une nouvelle question et décrivez précisément votre problème, car nous nous éloignons du sujet.

61voto

Pete Stensønes Points 2332

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

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

7 votes

Pour toute personne novice en matière de machines à états, il s'agit d'un excellent premier exemple pour se mouiller les pieds.

2 votes

Je suis nouveau dans le domaine des machines à états et sérieusement, ceci m'a apporté la lumière - merci !

2 votes

J'ai aimé cette mise en œuvre. Pour tous ceux qui pourraient tomber dessus, 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.

21voto

skrebbel Points 5183

C'est un peu de l'auto-promo éhontée ici, mais il y a quelque temps j'ai créé une bibliothèque appelée YieldMachine qui permet de décrire une machine à états de complexité limitée de manière très propre et simple. Prenons l'exemple d'une lampe :

state machine of a lamp

Remarquez que cette machine à états a 2 triggers et 3 états. Dans le code de la YieldMachine, nous écrivons une seule méthode pour tous les comportements liés aux états, dans laquelle nous commettons l'horrible atrocité d'utiliser la méthode goto pour 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 de 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();
    }
}

Court et agréable, hein !

Cette machine à états 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 parce que le compilateur C# a créé en interne une machine à états pour chaque méthode qui utilise la fonction yield return . Cette construction est généralement utilisée pour créer paresseusement des séquences de données, 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 entièrement nulle), mais par le comportement de l'état qui est créé sous le capot.

Le site StateMachine La classe de base effectue une réflexion sur la construction pour attribuer du code à chaque [Trigger] qui définit l'action Trigger et fait avancer la machine à états.

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

3 votes

Le "goto" n'est atroce que s'il saute entre les méthodes. Ce qui, heureusement, n'est pas autorisé en C#.

0 votes

Bien vu ! En fait, je serais très impressionné si un langage statiquement typé parvenait à autoriser un élément de type goto entre les méthodes.

3 votes

@Brannon : quelle langue permet goto pour passer d'une méthode à l'autre ? Je ne vois pas comment cela pourrait fonctionner. Non, goto est problématique parce qu'elle aboutit à une programmation procédurale (ce qui en soi complique des choses agréables comme les tests unitaires), favorise la répétition du code (remarquez comment InvalidTrigger doit être inséré pour chaque état ?) et enfin rend le déroulement du programme plus difficile à suivre. Comparez cette solution à (la plupart) des autres solutions de ce fil et vous verrez que c'est la seule où l'ensemble du FSM se déroule dans une seule méthode. C'est généralement suffisant pour soulever un problème.

14voto

Kevin Hsu Points 1288

Vous pouvez coder un bloc 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 décomposé n'a pas besoin de correspondre à quoi que ce soit, c'est juste 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 n'est encore exécuté. Ce que vous obtenez est en fait un générateur de machine à états, pour lequel vous pouvez créer une nouvelle instance de la machine à états. Pour ce faire, appelez GetEnumerator(). L'IEnumerator résultant est effectivement une machine à états que vous pouvez piloter en appelant MoveNext(...).

Ainsi, dans cet exemple, la première fois que vous appelez MoveNext(...) vous verrez "1" écrit dans la console, et la prochaine fois que vous appelez MoveNext(...) vous verrez 2, 3, 4, et puis 5, 6, 7 et puis 8, et puis 9, 10. Comme vous pouvez le constater, il s'agit d'un mécanisme utile pour orchestrer la façon dont les choses doivent se produire.

8 votes

Lien obligatoire vers avertissement juste

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X