Aktualisierung:
Nochmals vielen Dank für die Beispiele, sie waren sehr hilfreich und mit dem Folgenden möchte ich nichts von ihnen wegnehmen.
Sind'die derzeit gegebenen Beispiele, soweit ich sie verstehe'Zustandsmaschinen, nicht nur die Hälfte von dem, was wir normalerweise unter einer Zustandsmaschine verstehen?
In dem Sinne, dass die Beispiele zwar den Zustand ändern, dies aber nur durch die Änderung des Wertes einer Variablen dargestellt wird (und unterschiedliche Wertänderungen in verschiedenen Zuständen erlaubt), während normalerweise eine Zustandsmaschine auch ihr Verhalten ändern sollte, und zwar nicht (nur) im Sinne der Ermöglichung unterschiedlicher Wertänderungen für eine Variable je nach Zustand, sondern im Sinne der Ermöglichung der Ausführung unterschiedlicher Methoden für verschiedene Zustände.
Oder habe ich ein falsches Verständnis von Zustandsautomaten und ihrer allgemeinen Verwendung?
Mit freundlichen Grüßen
Ursprüngliche Frage:
Ich fand diese Diskussion über Zustandsautomaten & Iteratorblöcke in c# und Tools zum Erstellen von Zustandsautomaten und was nicht für C#, so fand ich eine Menge abstrakte Sachen, aber als Noob all dies ist ein wenig verwirrend.
Es wäre also toll, wenn jemand ein C#-Quellcode-Beispiel zur Verfügung stellen könnte, das einen einfachen Zustandsautomaten mit vielleicht 3,4 Zuständen realisiert, nur um den Kern der Sache zu verstehen.
Beginnen wir mit diesem einfachen Zustandsdiagramm:
Wir haben:
Sie können dies auf verschiedene Weise in C# konvertieren, z. B. indem Sie eine Switch-Anweisung für den aktuellen Zustand und Befehl ausführen oder Übergänge in einer Übergangstabelle nachschlagen. Für diesen einfachen Zustandsautomaten bevorzuge ich eine Übergangstabelle, die sich sehr einfach mit einem Wörterbuch darstellen lässt:
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();
}
}
}
Als persönliche Vorliebe entwerfe ich meine Zustandsautomaten gerne mit einer GetNext
-Funktion, um den nächsten Zustand deterministisch zurückzugeben, und einer MoveNext
-Funktion, um den Zustandsautomaten zu verändern.
Sie können einen Iteratorblock kodieren, der es Ihnen ermöglicht, einen Codeblock in einer orchestrierten Weise auszuführen. Die Art und Weise, wie der Codeblock aufgeteilt wird, muss nicht unbedingt mit irgendetwas übereinstimmen, es kommt nur darauf an, wie Sie ihn codieren wollen. Zum Beispiel:
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 Aufruf von CountToTen noch nichts wirklich ausgeführt. Was Sie erhalten, ist effektiv ein Zustandsmaschinengenerator, für den Sie eine neue Instanz der Zustandsmaschine erstellen können. Dies geschieht durch den Aufruf von GetEnumerator(). Der resultierende IEnumerator
So wird in diesem Beispiel beim ersten Aufruf von MoveNext(...) "1" in die Konsole geschrieben, und beim nächsten Aufruf von MoveNext(...) werden 2, 3, 4, und dann 5, 6, 7 und dann 8, und dann 9, 10 angezeigt. Wie Sie sehen können, ist dies ein nützlicher Mechanismus, um zu bestimmen, wie die Dinge ablaufen sollen.
Es ist nützlich, sich daran zu erinnern, dass Zustandsautomaten eine Abstraktion sind, und man braucht keine bestimmten Werkzeuge, um einen zu erstellen, aber Werkzeuge können nützlich sein.
Sie können zum Beispiel einen Zustandsautomaten mit 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 jagen und versuchen, sie mit Wasserballons zu treffen. Wenn sie nicht trifft, wird sie versuchen, einen abzufeuern, bis sie trifft (man sollte realistische Erwartungen haben ;)), ansonsten wird sie sich in der Konsole freuen. Sie jagt weiter, bis sie keine Möwen mehr hat, die sie belästigen kann.
Jede Funktion entspricht einem Zustand; die Start- und Endzustände (oder Akzeptanz) werden nicht angezeigt. Wahrscheinlich gibt es noch mehr Zustände, als durch die Funktionen modelliert werden. Zum Beispiel ist die Maschine nach dem Abfeuern des Ballons in Wirklichkeit in einem anderen Zustand als vorher, aber ich habe beschlossen, dass diese Unterscheidung unpraktisch ist.
Eine gängige Methode ist die Verwendung von Klassen zur Darstellung von Zuständen, die dann auf unterschiedliche Weise miteinander verbunden werden.