web-dev-qa-db-ja.com

C#で簡単なステートマシンの例?

更新:

例をありがとう、それらは非常に役に立ちました、そして、以下で私はそれらから何かを奪うつもりではありません。

現在与えられている例は、私がそれらとステートマシンを理解している限り、ステートマシンが通常理解しているものの半分しかないのではないでしょうか。
例は状態を変更するという意味では、変数の値を変更すること(そして異なる状態で異なる値を変更することを許可すること)によって表現されるだけです。状態に応じて変数に異なる値の変更を許可するという意味ではなく(異なるだけではなく)、異なる状態に対して異なるメソッドを実行できるようにするという意味です。

それとも、ステートマシンとその一般的な使用法について誤解していますか?

宜しくお願いします


元の質問:

ステートマシンとc#のイテレータブロック とステートマシンを作成するためのツール、そしてC#用ではないものに関するこの説明を見つけたので、たくさんの抽象的なものを見つけました。 。

ですから、誰かがC#ソースコードの例を提供して、3,4の状態を持つ単純なステートマシンを実現することができれば、その要旨を得ることができれば素晴らしいでしょう。


235
Jennifer Owens

この単純な状態図から始めましょう。

simple state machine diagram

我々は持っています:

  • 4つの状態(非アクティブ、アクティブ、一時停止、および終了)
  • 5種類の状態遷移(開始コマンド、終了コマンド、一時停止コマンド、再開コマンド、終了コマンド).

現在の状態とコマンドでswitchステートメントを実行したり、遷移テーブルで遷移を調べたりするなど、いくつかの方法でこれをC#に変換できます。この単純なステートマシンでは、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();
        }
    }
}

個人的な好みの問題として、次の状態を返すGetNext関数 確定的 と、状態機械を変化させるMoveNext関数を使ってステートマシンを設計します。

389
Juliet

あなたは既存のオープンソースのFinite State Machinesのうちの1つを使用したいと思うかもしれません。例えば。 bbv.Common.StateMachineは http://code.google.com/p/bbvcommon/wiki/StateMachine にあります。それは非常に直感的な流暢な構文とそのような開始/終了アクション、遷移アクション、ガード、階層的、受動的な実装(呼び出し側のスレッド上で実行される)および能動的な実装(fsmが実行される独自のスレッド)を持ちます。イベントはキューに追加されます。

Julietsを例にとると、ステートマシンの定義は非常に簡単になります。

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

更新:プロジェクトの場所は次の場所に移動しました。 https://github.com/appccelerate/statemachine

70
Remo Gloor

これは非常に単純化された有限状態機械の例で、非常に単純化された電子機器(テレビのような)をモデル化しています。

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; }
    }
}
}
48
Pete Stensønes

ここではいくつかの恥知らずな自己宣伝をしますが、少し前に、私は YieldMachine と呼ばれるライブラリを作成しました。たとえば、ランプを考えます:

state machine of a lamp

このステートマシンには2つのトリガーと3つのステートがあります。 YieldMachineコードでは、状態に関連するすべての動作に対して単一のメソッドを作成します。このメソッドでは、各状態にgotoを使用するという恐ろしい残虐行為を犯します。トリガーは、Actionという属性で装飾されたTrigger型のプロパティまたはフィールドになります。最初の状態のコードとその遷移を以下にコメントしました。次の州は同じパターンに従います。

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

短くていい、ええと!

このステートマシンは、単にトリガーを送信することによって制御されます。

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

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

明確にするために、私はあなたがこれを使用する方法を理解するのを助けるために最初の状態にいくつかのコメントを加えました。

    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

        ...

これは、C#コンパイラが実際にyield returnを使用する各メソッド用にステートマシンを内部的に作成したためです。このコンストラクトは通常、データのシーケンスを遅延的に作成するために使用されますが、この場合実際には返されるシーケンス(とにかくすべてnull)には関心がありませんが、フードの下で作成される状態の振る舞いに関心がありません。

StateMachine基本クラスは、各[Trigger]アクションにコードを割り当てるために、構造をある程度反映しています。これにより、Triggerメンバーが設定され、ステートマシンが前方に移動します。

しかし、あなたはそれを使うことができるように内部を理解する必要はありません。

20
skrebbel

コードブロックを体系的に実行できるようにする反復子ブロックをコーディングできます。コードブロックがどのように分割されるかは、実際には何にも対応する必要はなく、コード化する方法です。例えば:

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

この場合、CountToTenを呼び出しても、実際には何も実行されません。あなたが得るのは事実上ステートマシンジェネレータであり、それに対してステートマシンの新しいインスタンスを作成することができます。これを行うには、GetEnumerator()を呼び出します。結果のIEnumeratorは事実上、MoveNext(...)を呼び出すことで駆動できるステートマシンです。

したがって、この例では、最初にMoveNext(...)を呼び出したときにはコンソールに "1"が書き込まれ、次にMoveNext(...)を呼び出したときには2、3、4、と表示されます。お分かりのように、それは物事がどう起こるべきかを組織化するための有用なメカニズムです。

10
Kevin Hsu

これは別の観点からのステートマシンであるため、ここにイム投稿迷惑な答え。とても視覚的です。

私の元の答えは愚かな不完全なコードです。コードがステートマシンを視覚化するのを簡単にする配列のために行くので、私はそれがかなり視覚的だと思う。欠点はあなたがこれらすべてを書かなければならないということです。 Remos の答えはボイラープレートコードを書く手間を軽減しますが、それほど視覚的ではありません。 3番目の選択肢があります。本当にステートマシンを描いています。

.NETを使用していてランタイムのバージョン4をターゲットにできる場合は、ワークフローのステートマシンアクティビティを使用するオプションがあります。これらは本質的に( Juliet の図のように)ステートマシンを描画してWFランタイムに実行させることができます。

詳細については、MSDNの記事 Windows Workflow Foundationを使用したステートマシンの構築 を、最新バージョンについては このCodePlexサイト を参照してください。

.NETをターゲットにするときに私が常に好む選択肢は、プログラマには見えず、変更し、説明しやすいからです。彼らが言うように写真は千言葉の価値がある!

8
Pete Stensønes

ステートマシンは抽象化されていることを覚えておくと便利です。作成するのに特別なツールは必要ありませんが、便利な場合があります。

たとえば、次のような機能を備えたステートマシンを実現できます。

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

この機械はカモメを捜し、水風船でそれらを打つことを試みるでしょう。それが失敗するならば、それはそれがヒットするまでそれを発射しようとするでしょう(現実的な期待をすることですることができる;))、さもなければそれはコンソールで艶消しになるでしょう。それは嫌がらせをするためにカモメがなくなるまでそれは狩り続けます。

各機能は各状態に対応します。開始状態と終了状態(またはaccept)は表示されません。ただし、関数によってモデル化されたものよりも多くの状態がそこにあるでしょう。例えば、バルーンを発射した後は、マシンは実際には以前とは別の状態になっていますが、この区別は実用的ではないと判断しました。

一般的な方法は、状態を表すためにクラスを使用してから、それらをさまざまな方法で接続することです。

7
Skurmedel

今日はステートデザインのパターンに深く関わっています。 C#でのスレッド化 の画像で説明されているように、ThreadStateを実行してテストしました。

enter image description here

新しい状態を簡単に追加でき、ある状態から別の状態への移動を構成することは、状態の実装にカプセル化されているため非常に簡単です。

Atでの実装と使用: ステートデザインパターンによる.NET ThreadStateの実装

6
zzfima

この素晴らしいチュートリアルをオンラインで見つけて、それが私が有限状態機械に頭を包むのを助けました。

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

このチュートリアルは言語に依存しないので、C#のニーズに簡単に合わせることができます。

また、使用されている例(食べ物を探しているアリ)は理解しやすいです。


チュートリアルより:

enter image description here

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

    (...)
}
5
Jet Blue

私はまだC#で​​FSMを実装しようとしていませんが、これらはすべて私がCやASMのような低レベル言語で以前FSMを扱っていた方法とは非常に複雑です。

私がいつも知っている方法は「反復ループ」のようなものと呼ばれていると思います。その中には、基本的にイベント(割り込み)に基づいて定期的に終了してから再びメインループに戻る「while」ループがあります。

割り込みハンドラ内で、CurrentStateを渡してNextStateを返すと、メインループ内のCurrentState変数が上書きされます。プログラムが終了する(またはマイクロコントローラがリセットされる)まで、無限にこれを行います。

私が他の答えを見ているのは、FSMがどのように実装されることを意図しているのかと比較して非常に複雑に見えます。その美しさはその単純さにあり、FSMは多くの、多くの状態および遷移で非常に複雑になる可能性があるが、それらは複雑なプロセスを容易に分解し消化することを可能にする。

私は自分の回答に別の質問を含めるべきではないことを理解していますが、私は尋ねることを余儀なくされています。
彼らは巨大なそりハンマーで小さな釘を打つのに似ているようです。

5
dluberger

私はちょうどこれを貢献しました:

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

これは、IObserver(of signal)の状態を持つコマンドの直接的および間接的な送信をデモする例の1つです。したがって、シグナルソースIObservable(of signal)に応答します。

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

namespace Test
{
    using Machines;

    public static class WatchingTvSampleAdvanced
    {
        // Enum type for the transition triggers (instead of System.String) :
        public enum TvOperation { Plug, SwitchOn, SwitchOff, Unplug, Dispose }

        // The state machine class type is also used as the type for its possible states constants :
        public class Television : NamedState<Television, TvOperation, DateTime>
        {
            // Declare all the possible states constants :
            public static readonly Television Unplugged = new Television("(Unplugged TV)");
            public static readonly Television Off = new Television("(TV Off)");
            public static readonly Television On = new Television("(TV On)");
            public static readonly Television Disposed = new Television("(Disposed TV)");

            // For convenience, enter the default start state when the parameterless constructor executes :
            public Television() : this(Television.Unplugged) { }

            // To create a state machine instance, with a given start state :
            private Television(Television value) : this(null, value) { }

            // To create a possible state constant :
            private Television(string moniker) : this(moniker, null) { }

            private Television(string moniker, Television value)
            {
                if (moniker == null)
                {
                    // Build the state graph programmatically
                    // (instead of declaratively via custom attributes) :
                    Handler<Television, TvOperation, DateTime> stateChangeHandler = StateChange;
                    Build
                    (
                        new[]
                        {
                            new { From = Television.Unplugged, When = TvOperation.Plug, Goto = Television.Off, With = stateChangeHandler },
                            new { From = Television.Unplugged, When = TvOperation.Dispose, Goto = Television.Disposed, With = stateChangeHandler },
                            new { From = Television.Off, When = TvOperation.SwitchOn, Goto = Television.On, With = stateChangeHandler },
                            new { From = Television.Off, When = TvOperation.Unplug, Goto = Television.Unplugged, With = stateChangeHandler },
                            new { From = Television.Off, When = TvOperation.Dispose, Goto = Television.Disposed, With = stateChangeHandler },
                            new { From = Television.On, When = TvOperation.SwitchOff, Goto = Television.Off, With = stateChangeHandler },
                            new { From = Television.On, When = TvOperation.Unplug, Goto = Television.Unplugged, With = stateChangeHandler },
                            new { From = Television.On, When = TvOperation.Dispose, Goto = Television.Disposed, With = stateChangeHandler }
                        },
                        false
                    );
                }
                else
                    // Name the state constant :
                    Moniker = moniker;
                Start(value ?? this);
            }

            // Because the states' value domain is a reference type, disallow the null value for any start state value : 
            protected override void OnStart(Television value)
            {
                if (value == null)
                    throw new ArgumentNullException("value", "cannot be null");
            }

            // When reaching a final state, unsubscribe from all the signal source(s), if any :
            protected override void OnComplete(bool stateComplete)
            {
                // Holds during all transitions into a final state
                // (i.e., stateComplete implies IsFinal) :
                System.Diagnostics.Debug.Assert(!stateComplete || IsFinal);

                if (stateComplete)
                    UnsubscribeFromAll();
            }

            // Executed before and after every state transition :
            private void StateChange(IState<Television> state, ExecutionStep step, Television value, TvOperation info, DateTime args)
            {
                // Holds during all possible transitions defined in the state graph
                // (i.e., (step equals ExecutionStep.LeaveState) implies (not state.IsFinal))
                System.Diagnostics.Debug.Assert((step != ExecutionStep.LeaveState) || !state.IsFinal);

                // Holds in instance (i.e., non-static) transition handlers like this one :
                System.Diagnostics.Debug.Assert(this == state);

                switch (step)
                {
                    case ExecutionStep.LeaveState:
                        var timeStamp = ((args != default(DateTime)) ? String.Format("\t\t(@ {0})", args) : String.Empty);
                        Console.WriteLine();
                        // 'value' is the state value that we are transitioning TO :
                        Console.WriteLine("\tLeave :\t{0} -- {1} -> {2}{3}", this, info, value, timeStamp);
                        break;
                    case ExecutionStep.EnterState:
                        // 'value' is the state value that we have transitioned FROM :
                        Console.WriteLine("\tEnter :\t{0} -- {1} -> {2}", value, info, this);
                        break;
                    default:
                        break;
                }
            }

            public override string ToString() { return (IsConstant ? Moniker : Value.ToString()); }
        }

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

            // Create a signal source instance (here, a.k.a. "remote control") that implements
            // IObservable<TvOperation> and IObservable<KeyValuePair<TvOperation, DateTime>> :
            var remote = new SignalSource<TvOperation, DateTime>();

            // Create a television state machine instance (automatically set in a default start state),
            // and make it subscribe to a compatible signal source, such as the remote control, precisely :
            var tv = new Television().Using(remote);
            bool done;

            // Always holds, assuming the call to Using(...) didn't throw an exception (in case of subscription failure) :
            System.Diagnostics.Debug.Assert(tv != null, "There's a bug somewhere: this message should never be displayed!");

            // As commonly done, we can trigger a transition directly on the state machine :
            tv.MoveNext(TvOperation.Plug, DateTime.Now);

            // Alternatively, we can also trigger transitions by emitting from the signal source / remote control
            // that the state machine subscribed to / is an observer of :
            remote.Emit(TvOperation.SwitchOn, DateTime.Now);
            remote.Emit(TvOperation.SwitchOff);
            remote.Emit(TvOperation.SwitchOn);
            remote.Emit(TvOperation.SwitchOff, DateTime.Now);

            done =
                (
                    tv.
                        MoveNext(TvOperation.Unplug).
                        MoveNext(TvOperation.Dispose) // MoveNext(...) returns null iff tv.IsFinal == true
                    == null
                );

            remote.Emit(TvOperation.Unplug); // Ignored by the state machine thanks to the OnComplete(...) override above

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

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

注:この例はかなり人工的なもので、主にいくつかの直交する機能をデモするためのものです。このように、CRTPを使用して、状態値ドメイン自体を本格的なクラスで実装することが実際に必要になることはめったにありません( http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern を参照)。

これは、同じステートマシンで、同じテストケースを使用した、確かに単純で、より一般的な実装のユースケース(ステート値ドメインとして単純な列挙型を使用)です。

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

3
YSharp

なんてStatePatternなのか。それはあなたのニーズに合っていますか?

その文脈は関係していると思いますが、確かに一撃の価値があります。

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

これはあなたの州が「オブジェクト」クラスではなくどこへ行くべきかを決めることを可能にしました。

ブルーノ

3

私の考えでは、ステートマシンは状態を変更するためだけでなく、特定の状態内でトリガーやイベントを処理するためにも(非常に重要)意味があります。ステートマシンのデザインパターンをもっとよく理解したいのなら、本の中で良い説明を見つけることができます Head First Design Patterns、page 32

それは、変数内の状態だけでなく、異なる状態内のトリガーの処理についてもです。素晴らしい章(そしていいえ、これに言及するのに私には料金はかかりません:-)には、わかりやすい説明が含まれています。

2
Ton Snoei

Julietのコードからこれをgeneric state machineにしました。それは私にとって素晴らしい仕事です。

これらは利点です:

  • TStateTCommandという2つの列挙型を持つコードで、新しいステートマシンを作成できます。
  • [Try]GetNext()メソッドの出力結果をさらに制御するためにstruct TransitionResult<TState>を追加しました
  • 入れ子になったクラスStateTransitionを公開する のみからAddTransition(TState, TCommand, TState)まで使いやすくなりました。

コード:

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

これはTryGetNextメソッドの戻り型です。

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

使い方:

これがジェネリッククラスからOnlineDiscountStateMachineを作成する方法です。

その状態に対してenum OnlineDiscountStateを、そのコマンドに対してenum OnlineDiscountCommandを定義します。

これら2つのenumを使ってジェネリッククラスから派生したクラスOnlineDiscountStateMachineを定義します。

初期状態OnlineDiscountState.InitialStateに設定されるように、base(OnlineDiscountState.InitialState)からコンストラクターを派生させます。

必要な回数だけAddTransitionを使用する

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

派生ステートマシンを使用する

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

NuGetには2つの人気のあるステートマシンパッケージがあります。

Appccelerate.StateMachine (13.6Kのダウンロード+ 3.82Kのレガシーバージョン(bbv.Common.StateMachine))

StateMachineToolkit (1.56Kダウンロード)

Appccelerateライブラリには 優れたドキュメント がありますが、.NET 4はサポートされていないので、私は自分のプロジェクトにStateMachineToolkitを選びました。

0
Der_Meister

私は state.cs をお勧めします。私は個人的にstate.js(JavaScript版)を使っていてとても満足しています。そのC#バージョンも同様に機能します。

状態をインスタンス化します。

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

いくつかの遷移をインスタンス化します。

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

状態と遷移に対するアクションを定義します。

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

そしてそれは(ほとんど)それです。詳細についてはウェブサイトを見てください。

0
bmorin

Julietが提案したステートマシンには間違いがあると思います。メソッドGetHashCodeは、2つの異なる遷移に対して同じハッシュコードを返すことがあります。例えば、

状態=アクティブ(1)、コマンド=一時停止(2)=>ハッシュコード= 17 + 31 + 62 = 110

状態=一時停止(2)、コマンド=終了(1)=>ハッシュコード= 17 + 62 + 31 = 110

このエラーを回避するには、メソッドは次のようになります。

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

アレックス

0
alexag

FiniteStateMachineはC#で書かれた単純な状態機械です Link

利点は私のライブラリFiniteStateMachineを使うことです:

  1. 「コンテキスト」クラスを定義して、単一のインターフェースを外界に提示します。
  2. State抽象基本クラスを定義します。
  3. ステートマシンのさまざまな「状態」をState基本クラスの派生クラスとして表します。
  4. 適切なState派生クラスで状態固有の動作を定義します。
  5. "context"クラスの現在の "state"へのポインタを維持します。
  6. ステートマシンの状態を変更するには、現在の「状態」ポインタを変更します。

ダウンロードDLL ダウンロード

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;


     }

 }
0
Domenico Zinzi

このレポジトリの他の選択肢 https://github.com/lingkodsoft/StateBliss 流な構文を使用し、トリガーをサポートしています。

    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
mcdm