C#中的简单状态机示例?


257

更新:

再次感谢您提供的示例,它们非常有帮助,以下内容并不意味着我要从中脱颖而出。

就我所理解的例子和状态机而言,当前提供的示例不是我们通常对状态机了解的一半吗?
从某种意义上说,示例确实会改变状态,但这只能通过更改变量的值(并允许在不同状态下进行不同的值更改)来表示,而通常状态机也应该更改其行为,而行为(不是)允许变量根据状态而改变不同值的意义,但是就允许针对不同状态执行不同方法的意义而言。

还是我对状态机及其常见用法有误解?

最好的祝福


原始问题:

我发现了有关c#中的状态机和迭代器块以及用于创建状态机的工具的讨论,以及不适用于C#的讨论,因此我发现了很多抽象的东西,但是作为菜鸟,所有这些都有些令人困惑。

因此,如果有人可以提供一个C#源代码示例(实现一个可能具有3,4个状态的简单状态机)的主旨,那将是非常不错的。



您是否想知道一般的状态机还是只是基于迭代器的状态机?
Skurmedel

2
有净的核心无状态的lib结合实例,DAG的daigram等-值得总结:hanselman.com/blog/...
zmische

Answers:


416

让我们从这个简单的状态图开始:

简单状态机图

我们有:

  • 4种状态(无效,有效,已暂停和已退出)
  • 5种状态转换(开始命令,结束命令,暂停命令,恢复命令,退出命令)。

您可以通过几种方式将其转换为C#,例如对当前状态和命令执行switch语句,或在转换表中查找转换。对于这个简单的状态机,我更喜欢一个过渡表,该过渡表很容易使用来表示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功能以及使状态机变异的功能。


65
+1为GetHashCode()使用质数的正确实现。
2011年

13
您能否解释一下GetHashCode()的目的?
悉达多(Siddharth)2012年

14
@Siddharth:StateTransition该类在字典中用作键,键的相等性很重要。StateTransition只要两个不同的实例代表相同的过渡(例如CurrentStateCommand相同),就应视为相等。要实现平等,您必须重写EqualsGetHashCode。特别是字典将使用哈希码,并且两个相等的对象必须返回相同的哈希码。如果没有太多不相等的对象共享相同的哈希码,那么您也会获得良好的性能,这就是为什么如此GetHashCode实现。
Martin Liversage 2012年

14
虽然这肯定会为您提供状态机(以及适当的C#'ish实现),但我仍然认为它仍然缺少OP关于更改行为的问题的答案?毕竟,它只是计算状态,但是仍然缺少与状态更改有关的行为,程序的实际内容以及通常称为“进入/退出”事件。
stijn 2015年

2
如果有人需要它:我调整了此套针机,并在我的统一游戏中使用了它。它在git hub上可用:github.com/MarcoMig/Finite-State-Machine-FSM
Max_Power89 2015年

73

您可能要使用现有的开源有限状态机之一。例如,可以在http://code.google.com/p/bbvcommon/wiki/StateMachine中找到bbv.Common.StateMachine 。它具有非常直观的流利语法,并具有许多功能,例如进入/退出动作,过渡动作,防护,分层,被动实现(在调用者的线程上执行)和主动实现(在fsm运行所在的自己的线程,事件已添加到队列中)。

以朱丽叶为例,状态机的定义非常简单:

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


4
感谢您引用这个出色的开源状态机。请问如何获取当前状态?
拉马赞(Ramazan Polat)2012年

2
你不能,你不应该。状态是不稳定的。当您请求状态时,您可能处于过渡中间。所有动作都应在转换,状态进入和状态退出内完成。如果您确实想要状态,则可以添加本地字段并在输入操作中分配状态。
雷莫格洛尔

4
问题是您“需要”什么以及您是否真的需要SM状态或某种其他状态。例如,如果您需要一些显示文本,则多个陈述的内容可能具有相同的显示文本,例如,如果准备发送时具有多个子状态。在这种情况下,您应该完全按照自己的意愿去做。在正确的位置更新一些显示文本。例如在ExecuteOnEntry中。如果您需要更多信息,请提出一个新问题并准确说明您的问题,因为这是此处的主题。
Remo Gloor 2012年

好的,我要问一个新问题,等你答复。因为我没有别人可以解决这个问题,因为您的答案最好,但发问者仍然不接受。我将在此处发布问题网址。谢谢。
Ramazan Polat 2012年

4
流畅和声明性API的+1。这很棒。顺便说一句,谷歌代码似乎已经过时。其最新项目选址在GitHub 这里
自由联盟

51

这是一个非常经典的有限状态机的示例,它对非常简化的电子设备(例如电视)进行建模

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
对于刚接触状态机的人来说,这是一个很好的例子,它可以让您先弄湿脚。
PositiveGuy 2013年

2
我是状态机的新手,认真来说,这给我带来了光明-谢谢!
MC5 2013年

1
我喜欢这个实现。对于可能偶然发现此问题的任何人,都可以进行一些“改进”。在FSM类中,我添加了private void DoNothing() {return;}所有null实例并将其替换为this.DoNothing。具有返回当前状态的令人愉快的副作用。
Sethmo011

1
我想知道其中一些名称背后是否有推理依据。当我看这个时,我的直觉是将to的元素重命名StatesUnpowered, Standby, On。我的理由是,如果有人问我电视的状态是什么,我会说“关”而不是“开始”。我也改变StandbyWhenOnStandbyWhenOffTurnOnTurnOff。这使代码的阅读更加直观,但是我想知道是否存在使我的术语不太恰当的约定或其他因素。
杰森·哈姆耶

似乎合理,我并未真正遵循任何州命名约定;无论您使用哪种模型,都应使用as作为名称。
PeteStensønes19年

20

这里有些无耻的自我推广,但是不久前我创建了一个名为YieldMachine的库,该库允许以非常干净和简单的方式描述有限复杂性的状态机。例如,考虑一盏灯:

灯的状态机

请注意,此状态机具有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。通常使用这种构造来延迟创建数据序列,但是在这种情况下,我们实际上对返回的序列(无论如何都为空)并不感兴趣,而是对在幕后创建的状态行为感兴趣。

所述StateMachine基类确实上建设分配代码到每个一些反射[Trigger]动作,设定Trigger构件和所述状态机向前移动。

但是您实际上并不需要了解内部结构就能使用它。


2
只有在方法之间跳转时,“ goto”才是残酷的。幸运的是,这在C#中是不允许的。
布兰农

好点子!实际上,如果任何静态类型的语言都设法允许goto在方法之间进行转换,我将印象深刻。
斯克雷贝尔2013年

3
@布兰农:哪种语言允许goto在方法之间跳转?我不知道那将如何工作。不,这goto是有问题的,因为它会导致过程编程(这本身会使诸如单元测试之类的美好事情复杂化),促进代码重复(注意到InvalidTrigger每个状态都需要插入代码吗?)并最终使程序流程难以遵循。将其与该线程中的(大多数)其他解决方案进行比较,您将看到这是整个FSM以单一方法发生的唯一解决方案。通常这足以引起关注。
Groo 2014年

1
@Groo,例如GW-BASIC。它没有方法甚至函数都没有帮助。除此之外,我很难理解为什么在此示例中您会发现“程序流程更难遵循”。这是一台状态机,您要做的就是从另一个状态“进入”一个状态。这映射goto得很好。
skrebbel 2014年

3
GW-BASIC允许goto在功能之间跳转,但是它不支持功能吗?:)没错,“很难遵循”的说法更是一个普遍的goto问题,在这种情况下,实际上并不是那么多问题。
Groo 2014年

13

您可以对迭代器块进行编码,从而使您可以以协调的方式执行代码块。代码块的拆分方式实际上并不需要与任何内容相对应,这只是您要对其进行编码的方式。例如:

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和然后是5、6、7,然后是8,然后是9、10。如您所见,这是编排事情发生方式的有用机制。


6
强制性链接至公平警告
20:52

8

我在这里发布另一个答案,因为这是从不同角度来看的状态机。非常视觉化。

我最初的答案是经典命令式代码。我认为随着代码的发展,它的外观非常直观,这是因为数组使状态机的可视化变得简单。缺点是您必须编写所有这些内容。 雷莫斯(Remos)的回答减轻了编写样板代码的工作量,但视觉效果却差得多。还有第三种选择。真正绘制状态机。

如果您使用的是.NET并且可以指定运行时的版本4,则可以选择使用工作流的状态机活动。从本质上讲,这些方法使您可以绘制状态机(与Juliet的图一样),并让WF运行时为您执行它。

有关更多详细信息,请参见MSDN文章“ 使用Windows Workflow Foundation构建状态机”,有关最新版本,请参见此CodePlex网站

在针对.NET时,我总是会选择该选项,因为它易于查看,更改和向非程序员解释。图片值一千个字!


我认为状态机是整个工作流程基础中最好的部分之一!
fabsenet

7

记住状态机是一种抽象是很有用的,并且不需要特定的工具来创建状态机,但是工具可能会有用。

例如,您可以实现具有以下功能的状态机:

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

这台机器会搜寻海鸥,并尝试用水气球击中它们。如果未命中,它将尝试触发一个直到击中(可能符合一些现实的期望;),否则它将在控制台中幸灾乐祸。它会继续狩猎,直到被海鸥骚扰为止。

每个功能对应于每个状态;没有显示开始和结束(或接受)状态。尽管那里的状态可能比通过函数建模的状态还要多。例如,发射气球后,机器确实处于与之前不同的状态,但是我认为进行这种区分是不切实际的。

一种常见的方法是使用类表示状态,然后以不同的方式连接它们。


7

在网上找到了这个很棒的教程,它帮助我将注意力集中在有限状态机上。

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

本教程与语言无关,因此可以轻松地适应您的C#需求。

同样,所使用的示例(蚂蚁寻找食物)也易于理解。


从教程中:

在此处输入图片说明

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
尽管此链接可以回答问题,但最好在此处包括答案的基本部分,并提供链接以供参考。如果链接页面发生更改,仅链接的答案可能会无效。- 来自点评
drneel '16

@drneel我可以复制并粘贴本教程中的内容...但这不会使作者感到失望吗?
Jet Blue

1
@JetBlue:将答案中的链接留作参考,并在答案中用您自己的话语包含相关的内容,以免破坏任何人的版权。我知道这似乎很严格,但是由于这个规则,许多答案已经变得越来越好。
Flimm


5

我还没有尝试用C#实现FSM,但是这些听起来(或看上去)都非常复杂,这与我过去使用C或ASM等低级语言处理FSM的方式非常复杂。

我相信我一直知道的方法称为“迭代循环”。在其中,您实际上具有一个“ while”循环,该循环根据事件(中断)定期退出,然后再次返回主循环。

在中断处理程序中,您将传递CurrentState并返回NextState,该NextState会在主循环中覆盖CurrentState变量。您可以无限制地进行此操作,直到程序关闭(或微控制器复位)为止。

与我想实现的FSM相比,我看到的其他答案看起来都非常复杂。它的优点在于其简单性,并且FSM可能具有许多很多状态和过渡而非常复杂,但是它们使复杂的过程易于分解和消化。

我意识到我的回答不应包含其他问题,但我不得不问:为什么这些其他建议的解决方案看起来如此复杂?
它们似乎类似于用巨型八角锤打小钉子。


1
完全同意。带有switch语句的简单while循环非常简单。

2
除非您拥有一个具有许多状态和条件的非常复杂的状态机,否则最终将导致多个嵌套开关。另外,根据您的循环实现,繁忙等待中可能会有损失。
桑·里维尔斯


3

在我看来,状态机不仅用于更改状态,而且(非常重要)用于处理特定状态内的触发器/事件。如果您想更好地理解状态机设计模式,则可以在第320页的“ Head First Design Patterns ”一书中找到很好的描述。

它不仅涉及变量内的状态,还涉及处理不同状态内的触发器。很棒的一章(而且,对我来说,这是免费的:-),其中仅包含一个易于理解的解释。


3

我刚刚贡献了这一点:

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

这是演示直接和间接发送命令的示例之一,其状态为IObserver(信号),因此响应信号源IObservable(信号):

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


每个状态实例都有其自己的状态图副本是否有点奇怪?
Groo 2014年

@Groo:不,他们没有。只有使用私有构造函数(带有别名的空字符串)构造的电视实例(因此,称为受保护的“ Build”方法)才会具有状态图,作为状态机。其他被命名为电视的实例(对于常规和临时目的,其别名不为 null)将仅是“固定点”状态(可以这么说),用作状态常量(即实际状态机将引用为其顶点)。'HTH
YSharp 2014年

好的我明白了。无论如何,恕我直言,如果您包含一些实际处理这些转换的代码会更好。这样,它仅作为对库使用(IMHO)不太明显的接口的示例。例如,如何StateChange解决?通过反思?那真的有必要吗?
Groo 2014年

1
@Groo:好的话。确实没有必要在第一个示例中反思处理程序,因为它是在其中精确地以编程方式完成的,并且可以进行静态绑定/类型检查(与通过自定义属性不同)。因此,这项工作也符合预期: 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 2014年

1
谢谢你的努力!
Groo 2014年

3

我用朱丽叶的代码制作了这个通用状态机。这对我来说很棒。

这些是好处:

  • 您可以使用带有两个枚举TState和的代码创建新的状态机TCommand
  • 添加结构TransitionResult<TState>以更好地控制[Try]GetNext()方法的输出结果
  • 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从泛型类创建的方法:

OnlineDiscountState为状态定义一个枚举,OnlineDiscountCommand为命令定义一个枚举。

OnlineDiscountStateMachine使用这两个枚举定义从通用类派生的类

从中派生构造函数,base(OnlineDiscountState.InitialState)以便将初始状态设置为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);
    }

1

我认为朱丽叶(Juliet)提出的状态机有一个错误:GetHashCode方法可以为两个不同的转换返回相同的哈希码,例如:

状态=活动(1),命令=暂停(2)=>哈希码= 17 + 31 + 62 = 110

状态=暂停(2),命令=结束(1)=>哈希码= 17 + 62 + 31 = 110

为避免此错误,方法应如下所示:

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

亚历克斯


1
哈希码不需要为任何可能的组合返回唯一的数字,只需返回在目标范围内具有良好分布的唯一值即可(在这种情况下,范围是所有可能的int值)。这就是为什么HashCode总是与一起实现的原因Equals。如果哈希码相同,则使用Equals方法检查对象的精确性。
德米特里·阿夫托诺莫夫

0

FiniteStateMachine是一个简单的状态机,用C#链接编写

使用我的库FiniteStateMachine的优点:

  1. 定义一个“上下文”类,以提供与外界的单个接口。
  2. 定义一个State抽象基类。
  3. 将状态机的不同“状态”表示为State基类的派生类。
  4. 在适当的State派生类中定义特定于状态的行为。
  5. 保持指向“上下文”类中当前“状态”的指针。
  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;


     }

 }

1
它具有GNU GPL许可证。
Der_Meister

0

我会推荐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

此仓库中的其他替代方法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
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.