Prosty przykład automatu stanów w C #?


257

Aktualizacja:

Ponownie dziękuję za przykłady, które były bardzo pomocne i dlatego nie zamierzam niczego im odbierać.

Czy podane obecnie przykłady, o ile je rozumiem i automaty stanów, nie stanowią tylko połowy tego, co zwykle rozumiemy przez automaty stanów?
W tym sensie, że przykłady zmieniają stan, ale jest to reprezentowane tylko przez zmianę wartości zmiennej (i zezwalanie na różne zmiany wartości w różnych stanach), podczas gdy zwykle maszyna stanów powinna również zmieniać swoje zachowanie, a zachowanie nie (tylko) w poczucie umożliwienia różnych zmian wartości dla zmiennej w zależności od stanu, ale w sensie umożliwienia wykonania różnych metod dla różnych stanów.

Czy mam błędne wyobrażenie o automatach stanowych i ich powszechnym użyciu?

Z poważaniem


Oryginalne pytanie:

Znalazłem tę dyskusję o automatach stanowych i blokach iteratora w c # i narzędziach do tworzenia automatów stanowych, a co nie w języku C #, więc znalazłem wiele abstrakcyjnych rzeczy, ale jako noob wszystko to jest trochę mylące.

Byłoby więc wspaniale, gdyby ktoś mógł podać przykładowy kod źródłowy w języku C #, który realizuje prostą maszynę stanów z być może 3,4 stanami, tylko po to, aby uzyskać sedno.



Zastanawiasz się ogólnie nad automatami stanowymi, czy tylko z iteratorami?
Skurmedel

2
Istnieje .Net Core Stateless lib z przykładami, DAGs daigram itp. - warto przejrzeć: hanselman.com/blog/…
zmische

Odpowiedzi:


416

Zacznijmy od tego prostego diagramu stanu:

prosty schemat automatu stanów

Mamy:

  • 4 stany (nieaktywne, aktywne, wstrzymane i zakończone)
  • 5 rodzajów przejść stanów (polecenie Rozpocznij, Zakończ polecenie, Wstrzymaj polecenie, Wznów polecenie, Wyjdź polecenie).

Możesz przekonwertować to na C # na kilka sposobów, np. Wykonując instrukcję switch na bieżącym stanie i poleceniu lub przeglądając przejścia w tabeli przejścia. W przypadku tej prostej maszyny stanów wolę tabelę przejścia, którą bardzo łatwo jest przedstawić za pomocą 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();
        }
    }
}

W ramach osobistych preferencji lubię projektować moje maszyny stanów z GetNextfunkcją deterministycznego zwracania następnego stanu oraz MoveNextfunkcją mutowania maszyny stanów.


65
+1 za poprawne wdrożenie GetHashCode()używania liczb pierwszych.
ja72

13
Czy mógłbyś wyjaśnić mi cel GetHashCode ()?
Siddharth,

14
@ Siddharth: StateTransitionKlasa jest używana jako klucz w słowniku, a równość kluczy jest ważna. Dwa odrębne przypadki StateTransitionnależy uznać za równe, o ile reprezentują to samo przejście (np. CurrentStateI Commandsą takie same). W celu realizacji równości trzeba zastąpić Equals, jak również GetHashCode. W szczególności słownik użyje kodu skrótu, a dwa równe obiekty muszą zwrócić ten sam kod skrótu. Uzyskujesz również dobrą wydajność, jeśli nie wiele zbyt różnych obiektów ma ten sam kod skrótu, dlatego GetHashCodejest implementowany tak, jak pokazano.
Martin Liversage,

14
Chociaż to z pewnością daje ci maszynę stanową (i również poprawną implementację języka C #), czuję, że wciąż brakuje odpowiedzi na pytanie OP dotyczące zmiany zachowania? W końcu po prostu oblicza stany, ale nadal brakuje zachowania związanego ze zmianami stanu, faktycznego mięsa programu i zwykle nazywanych zdarzeniami Wejścia / Wyjścia.
stijn

2
Jeśli ktoś będzie go potrzebował: dostosowałem maszynę do gry w tate i użyłem jej w mojej grze o jedności. Jest dostępny na git hub: github.com/MarcoMig/Finite-State-Machine-FSM
Max_Power89

73

Możesz użyć jednego z istniejących skończonych automatów stanów. Np. Bbv.Common.StateMachine można znaleźć na stronie http://code.google.com/p/bbvcommon/wiki/StateMachine . Ma bardzo intuicyjną płynną składnię i wiele funkcji, takich jak akcje wejścia / wyjścia, akcje przejścia, zabezpieczenia, hierarchiczna, pasywna implementacja (wykonywana w wątku wywołującego) i aktywna implementacja (własny wątek, na którym działa fsm, zdarzenia są dodawane do kolejki).

Na przykładzie Juliets definicja maszyny stanowej staje się bardzo łatwa:

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

Aktualizacja : Lokalizacja projektu została przeniesiona do: https://github.com/appccelerate/statemachine


4
Dziękujemy za odwołanie się do tej doskonałej maszyny stanów open source. Czy mogę zapytać, jak mogę uzyskać obecny stan?
Ramazan Polat

2
Nie możesz i nie powinieneś. Stan jest czymś niestabilnym. Kiedy poprosisz o stan, możliwe, że jesteś w trakcie przejścia. Wszystkie działania powinny być wykonywane w ramach przejść, wejść i wyjść stanów. Jeśli naprawdę chcesz mieć stan, możesz dodać pole lokalne i przypisać stan w akcji wprowadzania.
Remo Gloor,

4
Pytanie dotyczy tego, czego „potrzebujesz” i czy naprawdę potrzebujesz stanu SM lub innego rodzaju stanu. Np. Jeśli potrzebujesz trochę wyświetlanego tekstu, to kilka stwierdzonych może mieć taki sam tekst wyświetlany, na przykład jeśli przygotowanie do wysłania ma wiele stanów podrzędnych. W takim przypadku powinieneś zrobić dokładnie to, co zamierzasz. Zaktualizuj wyświetlany tekst we właściwych miejscach. Np. W ExecuteOnEntry. Jeśli potrzebujesz więcej informacji, zadaj nowe pytanie i podaj dokładnie swój problem, ponieważ ten temat zaczyna się tutaj.
Remo Gloor,

Ok zadaję nowe pytanie i czekam na odpowiedź. Ponieważ nie sądzę, aby ktoś inny rozwiązał ten problem, ponieważ masz najlepszą odpowiedź, ale pytający nadal nie zaakceptował. Tutaj opublikuję adres URL pytania. Dzięki.
Ramazan Polat,

4
+1 za płynny i deklaratywny interfejs API. To jest zajebiste. BTW, kod Google wydaje się nieaktualny. Ich najnowsza strona projektu znajduje się na GitHub tutaj
KFL

51

Oto przykład bardzo klasycznej skończonej maszyny stanów, modelującej bardzo uproszczone urządzenie elektroniczne (jak telewizor)

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
dla każdego, kto nie zna maszyn stanowych, jest to doskonały pierwszy przykład zmoczenia stóp.
PositiveGuy,

2
Jestem nowy w automatach państwowych i poważnie, to przyniosło mi Światło - dzięki!
MC5,

1
Podobało mi się to wdrożenie. Dla każdego, kto może się na to natknąć, niewielka „poprawa”. W klasie FSM dodałem private void DoNothing() {return;}i zastąpiłem wszystkie wystąpienia wartości null wartością this.DoNothing. Ma przyjemny efekt uboczny powrotu do obecnego stanu.
Sethmo011,

1
Zastanawiam się, czy za niektórymi z tych nazwisk kryje się uzasadnienie. Kiedy na to patrzę, moją pierwszą intuicją jest zmiana nazwy elementów Statesna Unpowered, Standby, On. Moje rozumowanie jest takie, że gdyby ktoś zapytał mnie, w jakim stanie jest moja telewizja, powiedziałbym „Wyłącz”, a nie „Rozpocznij”. Zmieniłem również StandbyWhenOni StandbyWhenOffdo TurnOni TurnOff. Czyni to kod bardziej intuicyjnym, ale zastanawiam się, czy istnieją konwencje lub inne czynniki, które czynią moją terminologię mniej odpowiednią.
Jason Hamje

Wydaje się rozsądne, tak naprawdę nie przestrzegałem żadnej konwencji nazewnictwa stanów; nazwa ma sens dla każdego modelu.
Pete Stensønes,

20

Niektóre bezwstydne autopromocje tutaj, ale jakiś czas temu stworzyłem bibliotekę o nazwie YieldMachine, która pozwala na opisanie maszyny stanów o ograniczonej złożoności w bardzo czysty i prosty sposób. Rozważmy na przykład lampę:

automat państwowy lampy

Zauważ, że ta maszyna stanów ma 2 wyzwalacze i 3 stany. W kodzie YieldMachine piszemy jedną metodę dla wszystkich zachowań związanych ze stanem, w której popełniamy okropne okrucieństwo przy stosowaniu gotodla każdego stanu. Wyzwalacz staje się właściwością lub polem typu Actionozdobionym atrybutem o nazwie Trigger. Skomentowałem kod pierwszego stanu i jego przejścia poniżej; kolejne stany mają ten sam wzór.

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

Krótko i miło, eh!

Ta maszyna stanów jest kontrolowana po prostu przez wysyłanie do niej wyzwalaczy:

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

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

Aby to wyjaśnić, dodałem kilka komentarzy do pierwszego stanu, aby pomóc ci zrozumieć, jak z tego korzystać.

    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

        ...

Działa to, ponieważ kompilator C # faktycznie stworzył maszynę stanu wewnętrznie dla każdej używanej metody yield return . Konstrukt ten jest zwykle używany do leniwego tworzenia sekwencji danych, ale w tym przypadku tak naprawdę nie jesteśmy zainteresowani zwróconą sekwencją (która i tak ma wartość zerową), ale zachowaniem stanu, które powstaje pod maską.

StateMachineKlasa bazowa robi jakąś refleksję na temat budowy do kodu przypisać do każdego [Trigger]działania, które ustawiaTrigger element i przesuwa stan maszyny do przodu.

Ale tak naprawdę nie musisz rozumieć wewnętrznych elementów, aby móc z niego korzystać.


2
„Goto” jest okropne tylko wtedy, gdy przeskakuje między metodami. Na szczęście nie jest to dozwolone w języku C #.
Brannon

Słuszna uwaga! Byłbym pod wielkim wrażeniem, gdyby jakiemukolwiek statycznie typowanemu językowi udało się dopuścić gotomiędzy metodami.
skrebbel

3
@Brannon: który język pozwala gotoprzeskakiwać między metodami? Nie rozumiem, jak by to mogło działać. Nie, gotojest problematyczne, ponieważ powoduje programowanie proceduralne (samo w sobie komplikuje to miłe rzeczy, takie jak testowanie jednostkowe), promuje powtarzanie kodu (zauważyłeś, jak InvalidTriggernależy wstawić dla każdego stanu?) I ostatecznie utrudnia śledzenie programu. Porównaj to z (większością) innymi rozwiązaniami w tym wątku, a zobaczysz, że jest to jedyne, w którym cały FSM dzieje się za pomocą jednej metody. Zwykle to wystarcza, aby wzbudzić obawy.
Groo

1
@Groo, na przykład GW-BASIC. Pomaga to, że nie ma metod, a nawet funkcji. Poza tym bardzo trudno mi zrozumieć, dlaczego w tym przykładzie „trudniej jest śledzić przebieg programu”. To automat stanowy, „przejście do stanu z innego” jest jedyną rzeczą, którą robisz. To gotocałkiem dobrze odwzorowuje .
skrebbel

3
GW-BASIC pozwala gotona przeskakiwanie między funkcjami, ale nie obsługuje funkcji? :) Masz rację, uwaga „trudniejsza do naśladowania” jest bardziej ogólną gotokwestią, w tym przypadku nie stanowi większego problemu.
Groo

13

Możesz zakodować blok iteratora, który pozwala na wykonanie bloku kodu w zorganizowany sposób. To, w jaki sposób blok kodu jest rozbity, tak naprawdę wcale nie musi odpowiadać, po prostu chcesz go kodować. Na przykład:

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

W takim przypadku po wywołaniu CountToTen nic się jeszcze nie wykonuje. Dostajesz w efekcie generator maszyny stanów, dla którego możesz utworzyć nową instancję maszyny stanu. Robisz to, wywołując funkcję GetEnumerator (). Wynikowy IEnumerator jest w rzeczywistości maszyną stanu, którą można prowadzić, wywołując MoveNext (...).

Dlatego w tym przykładzie przy pierwszym wywołaniu MoveNext (...) zobaczysz napis „1” zapisany na konsoli, a przy następnym wywołaniu MoveNext (...) zobaczysz 2, 3, 4 i następnie 5, 6, 7, a następnie 8, a następnie 9, 10. Jak widać, jest to przydatny mechanizm do koordynowania tego, jak rzeczy powinny się zdarzyć.


6
Obowiązkowy Link do sprawiedliwego ostrzeżeniem
sehe

8

Podaję tutaj inną odpowiedź, ponieważ są to maszyny stanowe z innej perspektywy; bardzo wizualnie.

Moja oryginalna odpowiedź to klasyczny kod rozkazujący. Wydaje mi się, że kod jest dość wizualny ze względu na tablicę, która ułatwia wizualizację automatu stanów. Minusem jest to, że musisz to wszystko napisać. Remos zmniejsza wysiłek związany z pisaniem kodu płyty kotła, ale jest znacznie mniej wizualna. Istnieje trzecia alternatywa; naprawdę rysuje maszynę stanu.

Jeśli używasz platformy .NET i możesz kierować na wersję 4 środowiska wykonawczego, możesz skorzystać z działań maszyny stanu przepływu pracy . Te w istocie pozwalają narysować maszynę państwową (podobnie jak u Julii diagramie ) i umożliwić wykonanie jej przez WF.

Zobacz artykuł MSDN Budowanie automatów stanowych za pomocą Windows Workflow Foundation, aby uzyskać więcej informacji, oraz tę witrynę CodePlex dla najnowszej wersji.

Jest to opcja, którą zawsze wolałbym podczas atakowania na platformę .NET, ponieważ jest łatwa do zobaczenia, zmiany i wyjaśnienia osobom niebędącym programistami; zdjęcia są warte tysiąca słów, jak mówią!


Myślę, że automat stanowy jest jedną z najlepszych części całego fundamentu przepływu pracy!
fabsenet

7

Warto pamiętać, że maszyny stanowe są abstrakcją i nie potrzebujesz specjalnych narzędzi do ich tworzenia, jednak narzędzia mogą być przydatne.

Możesz na przykład zrealizować maszynę stanu z funkcjami:

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

Ta maszyna poluje na mewy i próbuje uderzyć je balonami wodnymi. Jeśli nie trafi, spróbuje odpalić jeden, dopóki nie trafi (może to przynieść pewne realistyczne oczekiwania;)), w przeciwnym razie będzie się chełpił w konsoli. Kontynuuje polowanie, dopóki nie opuści mew.

Każda funkcja odpowiada każdemu stanowi; stany początkowy i końcowy (lub akceptacja ) nie są wyświetlane. Prawdopodobnie jest tam więcej stanów niż modelowanych przez funkcje. Na przykład po odpaleniu balonu maszyna jest naprawdę w innym stanie niż przedtem, ale uznałem, że to rozróżnienie jest niepraktyczne.

Częstym sposobem jest użycie klas do reprezentowania stanów, a następnie połączenie ich na różne sposoby.


7

Znalazłem ten świetny samouczek online i pomógł mi owinąć głowę wokół maszyn o skończonych stanach.

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

Samouczek jest niezależny od języka, dzięki czemu można go łatwo dostosować do potrzeb języka C #.

Również zastosowany przykład (mrówka poszukująca jedzenia) jest łatwy do zrozumienia.


Z samouczka:

wprowadź opis zdjęcia tutaj

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
Chociaż ten link może odpowiedzieć na pytanie, lepiej jest dołączyć tutaj istotne części odpowiedzi i podać link w celach informacyjnych. Odpowiedzi zawierające tylko łącze mogą stać się nieprawidłowe, jeśli połączona strona ulegnie zmianie. - Z recenzji
drneel 21.04.16

@drneel Mógłbym kopiować i wklejać bity z samouczka ... ale czy to nie odbierałoby autorowi uznania?
Jet Blue

1
@JetBlue: Pozostaw link w odpowiedzi jako odniesienie i umieść odpowiednie bity we własnych słowach w odpowiedzi, aby nie łamać niczyich praw autorskich. Wiem, że wydaje się to surowe, ale wiele odpowiedzi stało się o wiele, wiele lepszych z powodu tej reguły.
Flimm 21.04.16


5

Nie próbowałem jeszcze implementować FSM w C #, ale wszystkie te dźwięki (lub wyglądają) są bardzo skomplikowane w stosunku do sposobu, w jaki obsługiwałem FSM w przeszłości w językach niskiego poziomu, takich jak C lub ASM.

Uważam, że metoda, którą zawsze znałem, nazywa się „pętlą iteracyjną”. Zasadniczo masz w nim pętlę „while”, która okresowo kończy się na podstawie zdarzeń (przerwań), a następnie wraca do głównej pętli.

W ramach procedur obsługi przerwań należy przekazać wartość CurrentState i zwrócić wartość NextState, która następnie zastąpi zmienną CurrentState w głównej pętli. Robisz to ad infinitum, dopóki program się nie zamknie (lub mikrokontroler się nie zresetuje).

To, co widzę w innych odpowiedziach, wygląda na bardzo skomplikowane w porównaniu z tym, w jaki sposób FSM jest moim zdaniem przeznaczony do wdrożenia; jego piękno tkwi w jego prostocie, a FSM może być bardzo skomplikowany w wielu, wielu stanach i przejściach, ale pozwalają one łatwo rozłożyć i strawić skomplikowany proces.

Zdaję sobie sprawę, że moja odpowiedź nie powinna zawierać innego pytania, ale jestem zmuszona zapytać: dlaczego te inne proponowane rozwiązania wydają się tak skomplikowane?
Wydaje się, że są podobni do uderzenia w mały gwóźdź gigantycznym młotem saneczkowym.


1
Zgadzać się w zupełności. Prosta pętla while z instrukcją switch jest tak prosta, jak to tylko możliwe.
rzuca

2
Chyba że masz bardzo skomplikowaną maszynę stanów z wieloma stanami i warunkami, w której skończyłbyś z wieloma zagnieżdżonymi przełącznikami. W zależności od implementacji pętli może zostać nałożona kara.
Sune Rievers

3

Co za stan StatePattern. Czy to pasuje do twoich potrzeb?

Myślę, że ma to związek z kontekstem, ale na pewno warto spróbować.

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

To pozwala twoim stanom decydować, dokąd pójść, a nie klasie „object”.

Bruno


1
Wzorzec stanu dotyczy klasy, która może działać inaczej w zależności od stanu / trybu, w którym się znajduje, nie zajmuje się przejściem między stanami.
Eli Algranti,

3

Moim zdaniem maszyna stanów służy nie tylko do zmiany stanów, ale także (bardzo ważne) do obsługi wyzwalaczy / zdarzeń w określonym stanie. Jeśli chcesz lepiej zrozumieć wzorzec projektowy maszyny stanów, dobry opis można znaleźć w książce Wzory projektowania pierwszego głowy, strona 320 .

Nie chodzi tylko o stany w zmiennych, ale także o obsługę wyzwalaczy w różnych stanach. Świetny rozdział (i nie, nie wspominam o tym za :-), który zawiera tylko łatwe do zrozumienia wyjaśnienie.


3

Właśnie włączyłem to:

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

Oto jeden z przykładów demonstrujących bezpośrednie i pośrednie wysyłanie poleceń, ze stanami jak IObserver (sygnału), a zatem odpowiada na źródło sygnału, IObservable (sygnału):

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

Uwaga: ten przykład jest raczej sztuczny i ma na celu głównie pokazanie szeregu funkcji ortogonalnych. Rzadko powinna istnieć rzeczywista potrzeba implementacji samej domeny wartości stanu za pomocą pełnej klasy, przy użyciu CRTP (patrz: http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern ) w ten sposób.

Oto z pewnością prostszy i prawdopodobnie znacznie bardziej powszechny przypadek użycia implementacji (użycie prostego typu wyliczenia jako domeny wartości stanów), dla tego samego automatu stanów i z tym samym przypadkiem testowym:

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


Czy to nie dziwne, że każda instancja stanu ma własną kopię grafu stanu?
Groo

@Groo: nie, nie robią tego. Tylko instancje telewizji zbudowane przy użyciu prywatnego konstruktora z ciągiem zerowym dla monikera (stąd wywołanie chronionej metody „kompilacji”) będą miały wykres stanu, jako maszyny stanu. Pozostali, nazwanych wystąpień Telewizji (z pseudonimem nie null dla konwencjonalnych i ad-hoc celem) będzie zaledwie „Punkt fix” państw (że tak powiem), służące jako stałych państwowych (że wykres państwo (-a) rzeczywiste maszyny stanu będą się odnosić jako ich wierzchołki). „HTH,
YSharp

Ok, rozumiem. W każdym razie, IMHO, byłoby lepiej, gdybyś dodał kod, który faktycznie obsługuje te przejścia. W ten sposób służy jedynie jako przykład użycia nieoczywistego interfejsu dla biblioteki. Na przykład jak to StateChangerozwiązać? Poprzez odbicie? Czy to naprawdę konieczne?
Groo

1
@Groo: Dobra uwaga. Rzeczywiście nie jest konieczne zastanawianie się nad programem obsługi w tym pierwszym przykładzie, ponieważ jest on tam wykonywany programowo dokładnie i można go statycznie powiązać / sprawdzić typ (w przeciwieństwie do atrybutów niestandardowych). Więc działa to również zgodnie z oczekiwaniami: 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
Dzięki za twój wysiłek!
Groo

3

Zrobiłem tę maszynę stanu ogólnego z kodu Juliet. Działa dla mnie niesamowicie.

Oto zalety:

  • możesz utworzyć nową maszynę stanu w kodzie z dwoma wierszami TStateiTCommand ,
  • dodano struct, TransitionResult<TState>aby mieć większą kontrolę nad wynikami wyjściowymi[Try]GetNext() metod
  • ujawnianie zagnieżdżonej klasy StateTransition tylko poprzez AddTransition(TState, TCommand, TState)ułatwienie pracy z nią

Kod:

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

To jest zwracany typ metody TryGetNext:

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

Jak używać:

W ten sposób możesz stworzyć OnlineDiscountStateMachine klasę ogólną:

Zdefiniuj wyliczenie OnlineDiscountStatedla swoich stanów i wyliczenieOnlineDiscountCommand dla jego poleceń.

Zdefiniuj klasę OnlineDiscountStateMachine wywodzącą się z klasy ogólnej za pomocą tych dwóch wyliczeń

Wyprowadzić konstruktor, base(OnlineDiscountState.InitialState)aby stan początkowy był ustawiony naOnlineDiscountState.InitialState

Używaj AddTransitiontyle razy, ile potrzeba

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

użyj pochodnej maszyny stanów

    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

Myślę, że maszyna stanu zaproponowana przez Juliet ma błąd: metoda GetHashCode może zwrócić ten sam kod skrótu dla dwóch różnych przejść, na przykład:

State = Active (1), Command = Pause (2) => HashCode = 17 + 31 + 62 = 110

State = Pauza (2), Command = End (1) => HashCode = 17 + 62 + 31 = 110

Aby uniknąć tego błędu, metoda powinna wyglądać następująco:

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

Alex


1
Kod skrótu nie jest wymagany do zwracania unikalnego numeru dla dowolnej możliwej kombinacji, a jedynie odrębnej wartości z dobrym rozkładem w całym zakresie docelowym (w tym przypadku zakres to wszystkie możliwe intwartości). Dlatego HashCodezawsze jest wdrażany wraz z Equals. Jeśli kody skrótu są takie same, obiekty są sprawdzane pod kątem dokładności za pomocą Equalsmetody.
Dmitry Avtonomov

0

FiniteStateMachine to prosta maszyna stanowa, napisana w C # Link

Zalety korzystania z mojej biblioteki FiniteStateMachine:

  1. Zdefiniuj klasę „kontekstową”, aby przedstawiać pojedynczy interfejs światu zewnętrznemu.
  2. Zdefiniuj podstawową klasę stanu.
  3. Reprezentują różne „stany” automatu stanowego jako pochodne klasy podstawowej klasy stanu.
  4. Zdefiniuj zachowanie właściwe dla stanu w odpowiednich klasach pochodnych od stanu.
  5. Zachowaj wskaźnik do bieżącego „stanu” w klasie „kontekstu”.
  6. Aby zmienić stan automatu stanów, zmień bieżący wskaźnik „stanu”.

Pobierz DLL do pobrania

Przykład na 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
Ma licencję GNU GPL.
Der_Meister

0

Polecam state.cs . Ja osobiście użyłem state.js (wersja JavaScript) i jestem z tego bardzo zadowolony. Ta wersja C # działa w podobny sposób.

Tworzysz stany:

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

Tworzysz niektóre przejścia:

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

Definiujesz działania dotyczące stanów i przejść:

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

I to jest (właściwie) to. Więcej informacji znajdziesz na stronie internetowej.



0

Inna alternatywa w tym repozytorium https://github.com/lingkodsoft/StateBliss używa płynnej składni, obsługuje wyzwalacze.

    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

Możesz skorzystać z mojego rozwiązania, jest to najwygodniejszy sposób. Jest również bezpłatny.

Utwórz maszynę stanu w trzech krokach:

1. Utwórz schemat w edytorze węzłów🔗 i załaduj go do projektu za pomocą biblioteki📚

StateMachine stateMachine = new StateMachine („schemat.xml”);

2. Opisz logikę aplikacji dotyczącą zdarzeń⚡

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

3. Uruchom maszynę stanu🚘

stateMachine.Start ();

Spinki do mankietów:

Edytor węzłów: https://github.com/SimpleStateMachine/SimpleStateMachineNodeEditor

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

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.