Einfaches Beispiel für eine Zustandsmaschine in C #?

257

Aktualisieren:

Nochmals vielen Dank für die Beispiele, sie waren sehr hilfreich und mit den folgenden möchte ich ihnen nichts wegnehmen.

Sind die derzeit gegebenen Beispiele, soweit ich sie verstehe, und Zustandsmaschinen nicht nur die Hälfte dessen, was wir normalerweise unter einer Zustandsmaschine verstehen?
In dem Sinne, dass die Beispiele den Status ändern, dies jedoch nur durch Ändern des Werts einer Variablen (und Zulassen unterschiedlicher Wertänderungen in unterschiedlichen Zuständen) dargestellt wird, während eine Zustandsmaschine normalerweise auch ihr Verhalten ändern sollte und das Verhalten nicht (nur) in Der Sinn, unterschiedliche Wertänderungen für eine Variable abhängig vom Zustand zuzulassen, aber der Sinn, unterschiedliche Methoden für verschiedene Zustände ausführen zu lassen.

Oder habe ich ein Missverständnis von Zustandsautomaten und ihrer allgemeinen Verwendung?

Freundliche Grüße


Ursprüngliche Frage:

Ich fand diese Diskussion über Zustandsautomaten und Iteratorblöcke in c # und Tools zum Erstellen von Zustandsautomaten und was nicht für C #, also fand ich viele abstrakte Dinge, aber als Noob ist das alles ein wenig verwirrend.

Es wäre also großartig, wenn jemand ein C # -Quellcodebeispiel bereitstellen könnte, das eine einfache Zustandsmaschine mit vielleicht 3,4 Zuständen realisiert, nur um das Wesentliche zu verstehen.


Jennifer Owens
quelle
Fragen Sie sich über Zustandsmaschinen im Allgemeinen oder nur über iteratorbasierte Maschinen?
Skurmedel
2
Es gibt .Net Core Stateless lib mit Beispielen, DAGs Daigramm usw. - eine Überprüfung wert: hanselman.com/blog/…
zmische

Antworten:

416

Beginnen wir mit diesem einfachen Zustandsdiagramm:

einfaches Zustandsmaschinendiagramm

Wir haben:

  • 4 Zustände (Inaktiv, Aktiv, Angehalten und Beendet)
  • 5 Arten von Statusübergängen (Startbefehl, Endbefehl, Pausenbefehl, Fortsetzungsbefehl, Beendigungsbefehl).

Sie können dies auf verschiedene Arten in C # konvertieren, z. B. indem Sie eine switch-Anweisung für den aktuellen Status und Befehl ausführen oder Übergänge in einer Übergangstabelle nachschlagen. Für diese einfache Zustandsmaschine bevorzuge ich eine Übergangstabelle, die mit a 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();
        }
    }
}

Aus persönlichen Gründen entwerfe ich meine Zustandsautomaten gerne mit einer GetNextFunktion, um den nächsten Zustand deterministisch zurückzugeben , und einer MoveNextFunktion, um die Zustandsmaschine zu mutieren.

Julia
quelle
66
+1 für die korrekte Implementierung der GetHashCode()Verwendung von Primzahlen.
Ja72
13
Könnten Sie mir bitte den Zweck von GetHashCode () erklären?
Siddharth
14
@Siddharth: Die StateTransitionKlasse wird als Schlüssel im Wörterbuch verwendet und die Gleichheit der Schlüssel ist wichtig. Zwei unterschiedliche Instanzen von StateTransitionsollten als gleich angesehen werden, solange sie denselben Übergang darstellen (z. B. CurrentStateund gleich Commandsind). Für die Umsetzung der Gleichstellung haben Sie außer Kraft zu setzen Equalssowie GetHashCode. Insbesondere verwendet das Wörterbuch den Hash-Code und zwei gleiche Objekte müssen denselben Hash-Code zurückgeben. Sie erhalten auch eine gute Leistung, wenn nicht zu viele ungleiche Objekte denselben Hash-Code verwenden, weshalb GetHashCodesie wie gezeigt implementiert werden.
Martin Liversage
14
Dies bringt Ihnen sicherlich eine Zustandsmaschine (und auch eine ordnungsgemäße C # 'ish-Implementierung), aber ich glaube, es fehlt immer noch die Antwort auf die Frage des OP nach Verhaltensänderungen? Schließlich werden nur Zustände berechnet, aber das Verhalten in Bezug auf Zustandsänderungen, das eigentliche Fleisch des Programms und normalerweise als Ein- / Ausstiegsereignisse bezeichnet, fehlt immer noch.
Stijn
2
Wenn es jemand braucht: Ich habe diese Tate-Maschine angepasst und in meinem Unity-Spiel verwendet. Es ist auf Git Hub verfügbar: github.com/MarcoMig/Finite-State-Machine-FSM
Max_Power89
73

Möglicherweise möchten Sie eine der vorhandenen Open-Source-Finite-State-Maschinen verwenden. Beispiel: bbv.Common.StateMachine unter http://code.google.com/p/bbvcommon/wiki/StateMachine . Es hat eine sehr intuitive, fließende Syntax und viele Funktionen wie Eingangs- / Ausgangsaktionen, Übergangsaktionen, Schutzfunktionen, hierarchische, passive Implementierung (auf dem Thread des Aufrufers ausgeführt) und aktive Implementierung (eigener Thread, auf dem der fsm ausgeführt wird). Ereignisse werden einer Warteschlange hinzugefügt).

Am Beispiel von Julia wird die Definition für die Zustandsmaschine sehr einfach:

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

Update : Der Projektspeicherort wurde verschoben zu: https://github.com/appccelerate/statemachine

Remo Gloor
quelle
4
Vielen Dank, dass Sie auf diese hervorragende Open Source-Zustandsmaschine verwiesen haben. Kann ich fragen, wie ich den aktuellen Status erhalten kann?
Ramazan Polat
3
Du kannst nicht und du solltest nicht. Staat ist etwas Instabiles. Wenn Sie den Status anfordern, befinden Sie sich möglicherweise mitten in einem Übergang. Alle Aktionen sollten innerhalb von Übergängen, Statuseintritten und Statusausgängen ausgeführt werden. Wenn Sie den Status wirklich haben möchten, können Sie ein lokales Feld hinzufügen und den Status in einer Eingabeaktion zuweisen.
Remo Gloor
4
Die Frage ist, wofür Sie es "brauchen" und ob Sie den SM-Staat oder eine andere Art von Staat wirklich brauchen. Wenn Sie beispielsweise Anzeigetext benötigen, können mehrere angegebene den gleichen Anzeigetext haben, z. B. wenn die Vorbereitung für das Senden mehrere Unterzustände aufweist. In diesem Fall sollten Sie genau das tun, was Sie beabsichtigen. Aktualisieren Sie den Anzeigetext an den richtigen Stellen. ZB innerhalb von ExecuteOnEntry. Wenn Sie weitere Informationen benötigen, stellen Sie eine neue Frage und geben Sie genau Ihr Problem an, da dies hier vom Thema abweicht.
Remo Gloor
Ok, ich stelle eine neue Frage und warte auf Ihre Antwort. Weil ich nicht glaube, dass jemand anderes dieses Problem löst, da Sie die beste Antwort haben, der Fragesteller dies aber immer noch nicht akzeptiert hat. Ich werde hier die URL der Frage posten. Vielen Dank.
Ramazan Polat
4
+1 für die fließende und deklarative API. Es ist toll. Übrigens scheint der Google-Code veraltet zu sein. Ihre neueste Projektseite ist auf GitHub hier
KFL
51

Hier ist ein Beispiel einer sehr klassischen Finite-State-Maschine, die ein sehr vereinfachtes elektronisches Gerät (wie einen Fernseher) modelliert.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace fsm
{
class Program
{
    static void Main(string[] args)
    {
        var fsm = new FiniteStateMachine();
        Console.WriteLine(fsm.State);
        fsm.ProcessEvent(FiniteStateMachine.Events.PlugIn);
        Console.WriteLine(fsm.State);
        fsm.ProcessEvent(FiniteStateMachine.Events.TurnOn);
        Console.WriteLine(fsm.State);
        fsm.ProcessEvent(FiniteStateMachine.Events.TurnOff);
        Console.WriteLine(fsm.State);
        fsm.ProcessEvent(FiniteStateMachine.Events.TurnOn);
        Console.WriteLine(fsm.State);
        fsm.ProcessEvent(FiniteStateMachine.Events.RemovePower);
        Console.WriteLine(fsm.State);
        Console.ReadKey();
    }

    class FiniteStateMachine
    {
        public enum States { Start, Standby, On };
        public States State { get; set; }

        public enum Events { PlugIn, TurnOn, TurnOff, RemovePower };

        private Action[,] fsm;

        public FiniteStateMachine()
        {
            this.fsm = new Action[3, 4] { 
                //PlugIn,       TurnOn,                 TurnOff,            RemovePower
                {this.PowerOn,  null,                   null,               null},              //start
                {null,          this.StandbyWhenOff,    null,               this.PowerOff},     //standby
                {null,          null,                   this.StandbyWhenOn, this.PowerOff} };   //on
        }
        public void ProcessEvent(Events theEvent)
        {
            this.fsm[(int)this.State, (int)theEvent].Invoke();
        }

        private void PowerOn() { this.State = States.Standby; }
        private void PowerOff() { this.State = States.Start; }
        private void StandbyWhenOn() { this.State = States.Standby; }
        private void StandbyWhenOff() { this.State = States.On; }
    }
}
}
Pete Stensønes
quelle
6
Für alle, die noch keine Erfahrung mit Zustandsmaschinen haben, ist dies ein hervorragendes erstes Beispiel, um zuerst die Füße nass zu machen.
PositiveGuy
2
Ich bin neu in Zustandsmaschinen und im Ernst, das hat mir das Licht gebracht - danke!
MC5
1
Diese Implementierung hat mir gefallen. Für alle, die darüber stolpern könnten, eine leichte "Verbesserung". In der FSM-Klasse habe ich private void DoNothing() {return;}alle Instanzen von null durch hinzugefügt und ersetzt this.DoNothing. Hat den angenehmen Nebeneffekt, den aktuellen Zustand wiederherzustellen.
Sethmo011
1
Ich frage mich, ob hinter einigen dieser Namen eine Begründung steckt. Als ich dies zu betrachten, meine erste Intuition ist , die Elemente des umbenennen Stateszu Unpowered, Standby, On. Meine Argumentation ist, dass wenn mich jemand fragen würde, in welchem ​​Zustand sich mein Fernseher befindet, ich "Aus" und nicht "Start" sagen würde. Ich habe mich auch verändert StandbyWhenOnund StandbyWhenOffzu TurnOnund TurnOff. Dadurch wird der Code intuitiver gelesen, aber ich frage mich, ob es Konventionen oder andere Faktoren gibt, die meine Terminologie weniger geeignet machen.
Jason Hamje
Scheint vernünftig, ich habe keine staatliche Namenskonvention befolgt; Name als macht Sinn für was auch immer Sie modellieren.
Pete Stensønes
20

Einige schamlose Eigenwerbung hier, aber vor einiger Zeit habe ich eine Bibliothek namens YieldMachine erstellt, mit der eine Zustandsmaschine mit begrenzter Komplexität auf sehr saubere und einfache Weise beschrieben werden kann. Stellen Sie sich zum Beispiel eine Lampe vor:

Zustandsmaschine einer Lampe

Beachten Sie, dass diese Zustandsmaschine 2 Trigger und 3 Zustände hat. In YieldMachine-Code schreiben wir eine einzige Methode für alle zustandsbezogenen Verhaltensweisen, bei der wir die schreckliche Gräueltat begehen, die gotofür jeden Zustand verwendet wird. Ein Trigger wird zu einer Eigenschaft oder einem Feld vom Typ Action, das mit einem Attribut namens dekoriert ist Trigger. Ich habe den Code des ersten Zustands und seine Übergänge unten kommentiert. Die nächsten Zustände folgen demselben Muster.

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

Kurz und nett, wie!

Diese Zustandsmaschine wird einfach durch Senden von Triggern an sie gesteuert:

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

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

Zur Verdeutlichung habe ich dem ersten Status einige Kommentare hinzugefügt, um Ihnen zu helfen, die Verwendung zu verstehen.

    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

        ...

Dies funktioniert, weil der C # -Compiler tatsächlich intern für jede verwendete Methode eine Zustandsmaschine erstellt hat yield return. Dieses Konstrukt wird normalerweise verwendet, um träge Datensequenzen zu erstellen. In diesem Fall interessiert uns jedoch nicht die zurückgegebene Sequenz (die ohnehin alle Nullen sind), sondern das Statusverhalten, das unter der Haube erstellt wird.

Die StateMachineBasisklasse reflektiert die Konstruktion, um jeder [Trigger]Aktion Code zuzuweisen , wodurch das TriggerElement festgelegt und die Zustandsmaschine vorwärts bewegt wird.

Aber Sie müssen die Interna nicht wirklich verstehen, um sie verwenden zu können.

skrebbel
quelle
2
Das "goto" ist nur dann grausam, wenn es zwischen den Methoden springt. Das ist in C # zum Glück nicht erlaubt.
Brannon
Guter Punkt! Tatsächlich wäre ich sehr beeindruckt, wenn es einer statisch typisierten Sprache gelingen würde, eine gotoZwischenmethode zuzulassen .
Skrebbel
3
@Brannon: Welche Sprache erlaubt es goto, zwischen Methoden zu springen? Ich sehe nicht ein, wie das möglicherweise funktionieren würde. Nein, gotoist problematisch, weil es zu einer prozeduralen Programmierung führt (dies allein erschwert nette Dinge wie das Testen von Einheiten), die Wiederholung von Code fördert (bemerkt, wie InvalidTriggerfür jeden Zustand eingefügt werden muss?) Und schließlich den Programmfluss schwieriger zu verfolgen macht. Vergleichen Sie dies mit (den meisten) anderen Lösungen in diesem Thread, und Sie werden sehen, dass dies die einzige ist, bei der der gesamte FSM in einer einzigen Methode ausgeführt wird. Das ist normalerweise genug, um Bedenken zu wecken.
Groo
1
@Groo, GW-BASIC zum Beispiel. Es hilft, dass es keine Methoden oder sogar Funktionen hat. Außerdem fällt es mir sehr schwer zu verstehen, warum Sie in diesem Beispiel den "Programmablauf schwerer zu verfolgen" finden. Es ist eine Zustandsmaschine, das "Gehen" zu einem Zustand von einem anderen ist das einzige, was Sie tun. Das passt gotoziemlich gut zu.
Skrebbel
3
GW-BASIC ermöglicht gotodas Wechseln zwischen Funktionen, unterstützt jedoch keine Funktionen? :) Sie haben Recht, die Bemerkung "schwerer zu folgen" ist eher ein allgemeines gotoProblem, in diesem Fall in der Tat kein so großes Problem.
Groo
13

Sie können einen Iteratorblock codieren, mit dem Sie einen Codeblock orchestriert ausführen können. Wie der Codeblock aufgebrochen wird, muss wirklich nichts entsprechen, es ist nur so, wie Sie ihn codieren möchten. Beispielsweise:

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 diesem Fall wird beim Aufrufen von CountToTen noch nichts ausgeführt. Was Sie erhalten, ist effektiv ein Zustandsmaschinengenerator, für den Sie eine neue Instanz der Zustandsmaschine erstellen können. Sie tun dies, indem Sie GetEnumerator () aufrufen. Der resultierende IEnumerator ist effektiv eine Zustandsmaschine, die Sie durch Aufrufen von MoveNext (...) steuern können.

In diesem Beispiel wird beim ersten Aufruf von MoveNext (...) "1" in die Konsole geschrieben, und beim nächsten Aufruf von MoveNext (...) werden 2, 3, 4 und angezeigt dann 5, 6, 7 und dann 8 und dann 9, 10. Wie Sie sehen können, ist dies ein nützlicher Mechanismus, um zu koordinieren, wie die Dinge ablaufen sollen.

Kevin Hsu
quelle
6
Obligatorischer Link zur fairen Warnung
siehe
8

Ich poste hier eine andere Antwort, da dies Zustandsmaschinen aus einer anderen Perspektive sind. sehr visuell.

Meine ursprüngliche Antwort ist klassischer Imperativcode. Ich denke, es ist ziemlich visuell, wie Code geht, wegen des Arrays, das die Visualisierung der Zustandsmaschine einfach macht. Der Nachteil ist, dass Sie das alles schreiben müssen. Die Antwort von Remos verringert den Aufwand beim Schreiben des Kesselplattencodes, ist jedoch weitaus weniger visuell. Es gibt die dritte Alternative; wirklich die Zustandsmaschine zeichnen.

Wenn Sie .NET verwenden und auf Version 4 der Laufzeit abzielen können, haben Sie die Möglichkeit, die Statusmaschinenaktivitäten des Workflows zu verwenden . Mit diesen können Sie im Wesentlichen die Zustandsmaschine zeichnen (ähnlich wie in Julias Diagramm) und von der WF-Laufzeit für Sie ausführen lassen.

Weitere Informationen finden Sie im MSDN-Artikel Erstellen von Statusmaschinen mit Windows Workflow Foundation und auf dieser CodePlex-Site für die neueste Version.

Dies ist die Option, die ich beim Targeting von .NET immer bevorzugen würde, da es für Nicht-Programmierer leicht zu sehen, zu ändern und zu erklären ist. Bilder sagen mehr als tausend Worte!

Pete Stensønes
quelle
Ich denke, die Zustandsmaschine ist einer der besten Teile der gesamten Workflow-Grundlage!
Fabsenet
7

Es ist nützlich, sich daran zu erinnern, dass Zustandsmaschinen eine Abstraktion sind und Sie keine bestimmten Werkzeuge benötigen, um eine zu erstellen. Werkzeuge können jedoch nützlich sein.

Sie können beispielsweise eine Zustandsmaschine mit folgenden Funktionen realisieren:

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

Diese Maschine würde nach Möwen suchen und versuchen, sie mit Wasserballons zu treffen. Wenn es fehlt, wird es versuchen, einen zu feuern, bis es trifft (könnte einige realistische Erwartungen haben;)), andernfalls wird es sich in der Konsole freuen. Es jagt weiter, bis keine Möwen mehr zu belästigen sind.

Jede Funktion entspricht jedem Zustand; Die Start- und Endzustände (oder Akzeptanzzustände ) werden nicht angezeigt. Es gibt dort wahrscheinlich mehr Zustände als von den Funktionen modelliert. Zum Beispiel befindet sich die Maschine nach dem Abfeuern des Ballons wirklich in einem anderen Zustand als zuvor, aber ich entschied, dass diese Unterscheidung unpraktisch war.

Eine übliche Methode besteht darin, Klassen zur Darstellung von Zuständen zu verwenden und diese dann auf unterschiedliche Weise zu verbinden.

Skurmedel
quelle
7

Ich habe dieses großartige Tutorial online gefunden und es hat mir geholfen, meinen Kopf um endliche Zustandsmaschinen zu wickeln.

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

Das Tutorial ist sprachunabhängig und kann daher problemlos an Ihre C # -Bedürfnisse angepasst werden.

Auch das verwendete Beispiel (eine Ameise, die nach Nahrung sucht) ist leicht zu verstehen.


Aus dem Tutorial:

Geben Sie hier die Bildbeschreibung ein

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

    (...)
}
Jet Blue
quelle
1
Während dieser Link die Frage beantworten kann, ist es besser, die wesentlichen Teile der Antwort hier aufzunehmen und den Link als Referenz bereitzustellen. Nur-Link-Antworten können ungültig werden, wenn sich die verknüpfte Seite ändert. - Von der Überprüfung
drneel
@drneel Ich könnte Teile aus dem Tutorial kopieren und einfügen ... aber würde das dem Autor nicht die Ehre nehmen?
Jet Blue
1
@JetBlue: Lassen Sie den Link in der Antwort als Referenz und fügen Sie die relevanten Bits in Ihre eigenen Wörter in den Antwortbeitrag ein, um das Urheberrecht von niemandem zu verletzen. Ich weiß, dass es streng erscheint, aber viele Antworten sind aufgrund dieser Regel viel, viel besser geworden.
Flimm
6

Heute bin ich tief in State Design Pattern. Ich habe ThreadState getestet, was (+/-) dem Threading in C # entspricht, wie im Bild aus Threading in C # beschrieben

Geben Sie hier die Bildbeschreibung ein

Sie können ganz einfach neue Status hinzufügen. Das Konfigurieren von Verschiebungen von einem Status in einen anderen ist sehr einfach, da diese in der Statusimplementierung enthalten sind

Implementierung und Verwendung unter: Implementiert .NET ThreadState nach Statusentwurfsmuster

zzfima
quelle
1
Link ist tot. Hast Du einen anderen?
rollt
5

Ich habe noch nicht versucht, ein FSM in C # zu implementieren, aber all dies klingt (oder sieht) sehr kompliziert aus, wie ich FSMs in der Vergangenheit in einfachen Sprachen wie C oder ASM gehandhabt habe.

Ich glaube, die Methode, die ich immer gekannt habe, heißt so etwas wie eine "iterative Schleife". Darin befindet sich im Wesentlichen eine 'while'-Schleife, die aufgrund von Ereignissen (Interrupts) regelmäßig beendet wird und dann wieder zur Hauptschleife zurückkehrt.

Innerhalb der Interrupt-Handler würden Sie einen CurrentState übergeben und einen NextState zurückgeben, der dann die CurrentState-Variable in der Hauptschleife überschreibt. Sie tun dies unendlich lange, bis das Programm geschlossen wird (oder der Mikrocontroller zurückgesetzt wird).

Was ich bei anderen Antworten sehe, sehen alle sehr kompliziert aus, verglichen mit der Art und Weise, wie ein FSM meiner Meinung nach implementiert werden soll. Seine Schönheit liegt in seiner Einfachheit und FSM kann mit vielen, vielen Zuständen und Übergängen sehr kompliziert sein, aber sie ermöglichen es, komplizierte Prozesse leicht zu zerlegen und zu verdauen.

Mir ist klar, dass meine Antwort keine weitere Frage enthalten sollte, aber ich bin gezwungen zu fragen: Warum scheinen diese anderen vorgeschlagenen Lösungen so kompliziert zu sein?
Sie scheinen einem kleinen Nagel mit einem riesigen Vorschlaghammer zu begegnen.

dluberger
quelle
1
Stimme voll und ganz zu. Eine einfache while-Schleife mit einer switch-Anweisung ist so einfach wie möglich.
rollt
2
Es sei denn, Sie haben eine sehr komplizierte Zustandsmaschine mit vielen Zuständen und Bedingungen, bei der Sie am Ende mehrere verschachtelte Schalter haben würden. Abhängig von Ihrer Schleifenimplementierung kann es auch zu einer Strafe beim Warten auf Besetzt kommen.
Sune Rievers
3

Was für ein Kampf gegen StatePattern. Passt das zu Ihren Bedürfnissen?

Ich denke, sein Kontext hängt zusammen, aber es ist auf jeden Fall einen Versuch wert.

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

Auf diese Weise können Ihre Bundesstaaten entscheiden, wohin sie gehen möchten, und nicht die "Objekt" -Klasse.

Bruno

Bruno Bertechini
quelle
1
Das Zustandsmuster befasst sich mit einer Klasse, die je nach Zustand / Modus, in dem sie sich befindet, unterschiedlich handeln kann. Es befasst sich nicht mit dem Übergang zwischen Zuständen.
Eli Algranti
3

Meiner Meinung nach ist eine Zustandsmaschine nicht nur zum Ändern von Zuständen gedacht, sondern auch (sehr wichtig) zum Behandeln von Triggern / Ereignissen innerhalb eines bestimmten Zustands. Wenn Sie das Entwurfsmuster für Zustandsmaschinen besser verstehen möchten, finden Sie eine gute Beschreibung im Buch Head First Design Patterns, Seite 320 .

Es geht nicht nur um die Zustände innerhalb von Variablen, sondern auch um die Behandlung von Triggern innerhalb der verschiedenen Zustände. Tolles Kapitel (und nein, es gibt keine Gebühr für mich, dies zu erwähnen :-), das nur eine leicht verständliche Erklärung enthält.

Ton Snoei
quelle
3

Ich habe gerade Folgendes beigetragen:

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

Hier ist eines der Beispiele, die das direkte und indirekte Senden von Befehlen mit dem Status IObserver (des Signals) demonstrieren und somit auf eine Signalquelle reagieren, IObservable (des Signals):

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

Hinweis: Dieses Beispiel ist eher künstlich und dient hauptsächlich dazu, eine Reihe von orthogonalen Merkmalen zu demonstrieren. Es sollte selten wirklich notwendig sein, die Statuswertdomäne selbst durch eine vollständige Klasse mithilfe des CRTP zu implementieren (siehe: http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern) ) wie .

Hier ist ein sicherlich einfacherer und wahrscheinlich viel häufigerer Anwendungsfall für die Implementierung (unter Verwendung eines einfachen Aufzählungstyps als Zustandswertdomäne) für dieselbe Zustandsmaschine und mit demselben Testfall:

https://code.google.com/p/ysharp/source/browse/trunk/StateMachinesPoC/WatchingTVSample.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Test
{
    using Machines;

    public static class WatchingTvSample
    {
        public enum Status { Unplugged, Off, On, Disposed }

        public class DeviceTransitionAttribute : TransitionAttribute
        {
            public Status From { get; set; }
            public string When { get; set; }
            public Status Goto { get; set; }
            public object With { get; set; }
        }

        // State<Status> is a shortcut for / derived from State<Status, string>,
        // which in turn is a shortcut for / derived from State<Status, string, object> :
        public class Device : State<Status>
        {
            // Executed before and after every state transition :
            protected override void OnChange(ExecutionStep step, Status value, string info, object args)
            {
                if (step == ExecutionStep.EnterState)
                {
                    // 'value' is the state value that we have transitioned FROM :
                    Console.WriteLine("\t{0} -- {1} -> {2}", value, info, this);
                }
            }

            public override string ToString() { return Value.ToString(); }
        }

        // Since 'Device' has no state graph of its own, define one for derived 'Television' :
        [DeviceTransition(From = Status.Unplugged, When = "Plug", Goto = Status.Off)]
        [DeviceTransition(From = Status.Unplugged, When = "Dispose", Goto = Status.Disposed)]
        [DeviceTransition(From = Status.Off, When = "Switch On", Goto = Status.On)]
        [DeviceTransition(From = Status.Off, When = "Unplug", Goto = Status.Unplugged)]
        [DeviceTransition(From = Status.Off, When = "Dispose", Goto = Status.Disposed)]
        [DeviceTransition(From = Status.On, When = "Switch Off", Goto = Status.Off)]
        [DeviceTransition(From = Status.On, When = "Unplug", Goto = Status.Unplugged)]
        [DeviceTransition(From = Status.On, When = "Dispose", Goto = Status.Disposed)]
        public class Television : Device { }

        public static void Run()
        {
            Console.Clear();

            // Create a television state machine instance, and return it, set in some start state :
            var tv = new Television().Start(Status.Unplugged);
            bool done;

            // Holds iff the chosen start state isn't a final state :
            System.Diagnostics.Debug.Assert(tv != null, "The chosen start state is a final state!");

            // Trigger some state transitions with no arguments
            // ('args' is ignored by this state machine's OnChange(...), anyway) :
            done =
                (
                    tv.
                        MoveNext("Plug").
                        MoveNext("Switch On").
                        MoveNext("Switch Off").
                        MoveNext("Switch On").
                        MoveNext("Switch Off").
                        MoveNext("Unplug").
                        MoveNext("Dispose") // MoveNext(...) returns null iff tv.IsFinal == true
                    == null
                );

            Console.WriteLine();
            Console.WriteLine("Is the TV's state '{0}' a final state? {1}", tv.Value, done);

            Console.WriteLine();
            Console.WriteLine("Press any key...");
            Console.ReadKey();
        }
    }
}

'HTH

YSharp
quelle
Ist es nicht ein bisschen seltsam, dass jede Zustandsinstanz eine eigene Kopie des Zustandsgraphen hat?
Groo
@Groo: Nein, das tun sie nicht. Nur die Instanzen von Television, die mit dem privaten Konstruktor mit einer Nullzeichenfolge für den Moniker erstellt wurden (daher die geschützte 'Build'-Methode aufrufen), haben ein Zustandsdiagramm als Zustandsautomaten. Die anderen, benannten Instanzen des Fernsehens (mit einem Spitznamen , der für diesen konventionellen und Ad-hoc-Zweck nicht null ist), sind (sozusagen) bloße "Fixpunkt" -Zustände, die als Zustandskonstanten (die Zustandsgraphen von) dienen Ist-Zustandsmaschinen werden als ihre Eckpunkte bezeichnet. 'HTH,
YSharp
Okay, ich habs verstanden. Wie auch immer, meiner Meinung nach wäre es besser gewesen, wenn Sie Code eingefügt hätten, der diese Übergänge tatsächlich handhabt. Auf diese Weise dient es nur als Beispiel für die Verwendung einer (IMHO) nicht so offensichtlichen Schnittstelle für Ihre Bibliothek. Wie wird zum Beispiel StateChangegelöst? Durch Reflexion? Ist das wirklich notwendig?
Groo
1
@Groo: Gute Bemerkung. Es ist in der Tat nicht notwendig, über den Handler in diesem ersten Beispiel nachzudenken, da er dort genau programmgesteuert ausgeführt wird und statisch gebunden / typgeprüft werden kann (im Gegensatz zu benutzerdefinierten Attributen). Also diese Arbeit auch wie erwartet: 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
Danke für deinen Einsatz!
Groo
3

Ich habe diese generische Zustandsmaschine aus Julias Code gemacht. Es funktioniert großartig für mich.

Dies sind die Vorteile:

  • Sie können eine neue Zustandsmaschine im Code mit zwei Aufzählungen TStateund erstellen TCommand.
  • Struktur hinzugefügt TransitionResult<TState>, um mehr Kontrolle über die Ausgabeergebnisse von [Try]GetNext()Methoden zu haben
  • Verschachtelte Klasse StateTransition nur verfügbar AddTransition(TState, TCommand, TState)machen, indem die Arbeit damit erleichtert wird

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

Dies ist der Rückgabetyp der TryGetNext-Methode:

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

Wie benutzt man:

So können Sie OnlineDiscountStateMachineaus der generischen Klasse eine erstellen :

Definieren Sie eine Aufzählung OnlineDiscountState für seine Zustände und eine Aufzählung OnlineDiscountCommandfür ihre Befehle.

Definieren Sie eine Klasse OnlineDiscountStateMachine die von der generischen Klasse abgeleitet ist, indem Sie diese beiden Aufzählungen verwenden

Leiten Sie den Konstruktor base(OnlineDiscountState.InitialState)so ab, dass der Anfangszustand auf gesetzt istOnlineDiscountState.InitialState

Verwenden Sie AddTransitionso oft wie nötig

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

Verwenden Sie die abgeleitete Zustandsmaschine

    odsm = new OnlineDiscountStateMachine();
    public void Connect()
    {
        var result = odsm.TryGetNext(OnlineDiscountCommand.Connect);

        //is result valid?
        if (!result.IsValid)
            //if this happens you need to add transitions to the state machine
            //in this case result.NewState is the same as before
            Console.WriteLine("cannot navigate from this state using OnlineDiscountCommand.Connect");

        //the transition was successfull
        //show messages for new states
        else if(result.NewState == OnlineDiscountState.Error_AuthenticationError)
            Console.WriteLine("invalid user/pass");
        else if(result.NewState == OnlineDiscountState.Connected)
            Console.WriteLine("Connected");
        else
            Console.WriteLine("not implemented transition result for " + result.NewState);
    }
Bizhan
quelle
1

Ich denke, die von Juliet vorgeschlagene Zustandsmaschine hat einen Fehler: Die Methode GetHashCode kann denselben Hashcode für zwei verschiedene Übergänge zurückgeben, zum Beispiel:

Status = Aktiv (1), Befehl = Pause (2) => HashCode = 17 + 31 + 62 = 110

Status = Angehalten (2), Befehl = Ende (1) => HashCode = 17 + 62 + 31 = 110

Um diesen Fehler zu vermeiden, sollte die Methode folgendermaßen aussehen:

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

Alex

alexag
quelle
1
Hash-Code ist nicht erforderlich, um eine eindeutige Nummer für eine mögliche Kombination zurückzugeben, sondern nur einen eindeutigen Wert mit guter Verteilung über den Zielbereich (in diesem Fall sind alle möglichen intWerte der Bereich). Deshalb HashCodewird immer zusammen mit implementiert Equals. Wenn die Hash-Codes identisch sind, werden die Objekte mit der EqualsMethode auf exakte Gleichheit überprüft .
Dmitry Avtonomov
0

FiniteStateMachine ist eine einfache Zustandsmaschine, die in C # Link geschrieben ist

Vorteile bei der Nutzung meiner Bibliothek FiniteStateMachine:

  1. Definieren Sie eine "Kontext" -Klasse, um der Außenwelt eine einzige Schnittstelle zu präsentieren.
  2. Definieren Sie eine abstrakte Basisklasse für den Status.
  3. Stellen Sie die verschiedenen "Zustände" der Zustandsmaschine als abgeleitete Klassen der Zustandsbasisklasse dar.
  4. Definieren Sie zustandsspezifisches Verhalten in den entsprechenden vom Status abgeleiteten Klassen.
  5. Pflegen Sie einen Zeiger auf den aktuellen "Status" in der Klasse "Kontext".
  6. Um den Status der Zustandsmaschine zu ändern, ändern Sie den aktuellen "Status" -Zeiger.

Download DLL Download

Beispiel auf LINQPad:

void Main()
{
            var machine = new SFM.Machine(new StatePaused());
            var output = machine.Command("Input_Start", Command.Start);
            Console.WriteLine(Command.Start.ToString() + "->  State: " + machine.Current);
            Console.WriteLine(output);

            output = machine.Command("Input_Pause", Command.Pause);
            Console.WriteLine(Command.Pause.ToString() + "->  State: " + machine.Current);
            Console.WriteLine(output);
            Console.WriteLine("-------------------------------------------------");
}
    public enum Command
    {
        Start,
        Pause,
    }

    public class StateActive : SFM.State
    {

        public override void Handle(SFM.IContext context)

        {
            //Gestione parametri
            var input = (String)context.Input;
            context.Output = input;

            //Gestione Navigazione
            if ((Command)context.Command == Command.Pause) context.Next = new StatePaused();
            if ((Command)context.Command == Command.Start) context.Next = this;

        }
    }


public class StatePaused : SFM.State
{

     public override void Handle(SFM.IContext context)

     {

         //Gestione parametri
         var input = (String)context.Input;
         context.Output = input;

         //Gestione Navigazione
         if ((Command)context.Command == Command.Start) context.Next = new  StateActive();
         if ((Command)context.Command == Command.Pause) context.Next = this;


     }

 }
Domenico Zinzi
quelle
1
Es hat GNU GPL Lizenz.
Der_Meister
0

Ich würde state.cs empfehlen . Ich persönlich habe state.js (die JavaScript-Version) verwendet und bin sehr zufrieden damit. Diese C # -Version funktioniert auf ähnliche Weise.

Sie instanziieren Zustände:

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

Sie instanziieren einige Übergänge:

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

Sie definieren Aktionen für Zustände und Übergänge:

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

Und das war's (so ziemlich). Weitere Informationen finden Sie auf der Website.

bmorin
quelle
0

Es gibt 2 beliebte Zustandsmaschinenpakete in NuGet.

Appccelerate.StateMachine (13,6 KB Downloads + 3,82 KB Legacy-Version (bbv.Common.StateMachine))

StateMachineToolkit (1.56K Downloads)

Die Appccelerate-Bibliothek verfügt über eine gute Dokumentation , unterstützt jedoch nicht .NET 4, sodass ich für mein Projekt StateMachineToolkit ausgewählt habe.

Der_Meister
quelle
0

Eine andere Alternative in diesem Repo https://github.com/lingkodsoft/StateBliss verwendet fließende Syntax, unterstützt 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
    }
}
mcdm
quelle
0

Sie können meine Lösung verwenden, dies ist der bequemste Weg. Es ist auch kostenlos.

Erstellen Sie die Zustandsmaschine in drei Schritten:

1. Erstellen Sie ein Schema im Knoteneditor und laden Sie es mithilfe der Bibliothek in Ihr Projekt

StateMachine stateMachine = neue StateMachine ("Schema.xml");

2. Beschreiben Sie Ihre App-Logik für Ereignisse

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

3. Führen Sie die Zustandsmaschine aus

stateMachine.Start ();

Links:

Knoteneditor: https://github.com/SimpleStateMachine/SimpleStateMachineNodeEditor

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

GMIKE
quelle