【问题标题】:What's the best(when performance matters) way to implement a state machine in C#?在 C# 中实现状态机的最佳(当性能很重要)方法是什么?
【发布时间】:2010-12-09 17:27:47
【问题描述】:

我想出了以下选项:

使用 goto 语句:

Start:
    goto Data
Data:
    goto Finish
Finish:
    ;

使用 switch 语句:

switch(m_state) {
    case State.Start:
        m_state = State.Data;
        break;
    case State.Data:            
        m_state = State.Finish;
        break;
    case State.Finish:
        break;
}

同时使用 goto 和 switch:

switch(m_state) {
    case State.Start:
        goto case State.Data2;
    case State.Data1:
        goto case State.Finish;
    case State.Data2:
        m_state = State.Data1;
        //call to a function outside the state machine
        //that could possibly change the state
        break;
    case State.Finish:
        break;
}

我更喜欢使用 goto 语句的第一个选项,因为它更快且更简洁。但我不确定这是否是最佳选择。也许性能明智,但谈到可读性我不知道。这就是我问这个问题的原因。您更喜欢哪个选项,为什么?

【问题讨论】:

  • 您的状态机是要主动执行,还是会受到来自外部的推动? label 方法表明您的状态机将积极地继续执行并拉动冲动,但可能会使处理不受其控制的外部影响变得更加困难。我会说使用 switch 语句,但还有其他方法。
  • 谨防在此处大声说出 goto 字样。总是有代码纯粹主义者潜伏着等待最小的理由开始抱怨他们宝贵的模式和最佳实践。 Goto 在他们的书中是最糟糕的...... ;^)
  • 它将继续积极执行发射令牌。根据从外部设置的标志,它会在必要时更改状态。
  • 我相信他们可以看到在状态机中使用 goto 的好处。反正你会到处跳。
  • niek:我更喜欢第一个。它尽可能简洁地传达您想要实现的目标。您可能会选择在每个标签之后启动本地范围(因此 { .... } ),这样您就可以在状态机的每个步骤中使用局部变量

标签: c# switch-statement goto state-machine


【解决方案1】:

如果您想将状态机转换逻辑分解为单独的函数,则只能使用 switch 语句来实现。

switch(m_state) {
        case State.Start:
                m_state = State.Data;
                break;
        case State.Data:                        
                m_state = ComputeNextState();
                break;
        case State.Finish:
                break;
} 

它也更具可读性,switch 语句(相对于 Goto)的开销只会在极少数情况下产生性能差异。

编辑:

您可以使用“goto case”来进行小的性能改进:

switch(m_state) {
        case State.Start:
                m_state = State.Data; // Don't forget this line!
                goto case State.Data;
        case State.Data:                        
                m_state = ComputeNextState();
                break;
        case State.Finish:
                break;
} 

但是,您可能会忘记更新状态变量。这可能会在以后导致细微的错误(因为您假设设置了“m_state”),所以我建议避免它。

【讨论】:

  • 请注意,goto case 的用法与“将gotoswitch 混合使用”不同,它没有相同的缺点——事实上,它会产生清晰的,结构化代码,实际上是一个很好的解决方案。
  • 我添加了一个解释,说明为什么我不喜欢 goto case 与 switch 语句混合。
【解决方案2】:

我更喜欢相互调用/递归函数。调整您的示例:

returnvalue Start() {
    return Data();
}

returnvalue Data() {
    return Finish();
}

returnvalue Finish() {
    …
}

理论上,这个 可以 完全内联,以便编译器输出等同于您的 goto 解决方案(因此,速度相同)。实际上,the C# compiler /JITter probably won’t do it。但是由于该解决方案的可读性要高得多(好吧,恕我直言),我只会在经过非常仔细的基准测试证明它在速度方面确实较差之后,用goto 解决方案替换它,或者发生堆栈溢出(不是在这个简单的解决方案中,而是在更大的自动机中遇到这个问题)。

即便如此,我肯定会坚持使用goto case 解决方案。为什么?因为那样你的整个凌乱的goto 意大利面被很好地封装在一个块结构(switch 块)中,你的意大利面条不会破坏其余的代码,防止肉酱。

结论:功能变体很明确,但通常容易出现问题。 goto 解决方案很混乱。只有goto case 提供了半干净、高效的解决方案。如果性能确实是最重要的(而自动机是瓶颈),请选择结构化的goto case 变体。

【讨论】:

  • 不记得 c# 有尾递归,所以我绝对建议不要使用这种方法
  • reinier: first off:虽然 C# 不做 TCO,但 JITter 在某些情况下会做:blogs.msdn.com/davbr/pages/tail-call-jit-conditions.aspxsecond Don' t 高估非虚拟(静态?)方法调用的成本。 第三这就是我建议进行基准测试的原因。总之,这个答案应该如何被否决超出了我的范围。它对问题及其解决方案进行了相当完整的讨论。
  • 康拉德:很多可能。如果你不知道jitter会优化它,那就意味着你不能使用它。拥有一个缓慢运行的堆栈确实是您不想要的。发生这种情况的速度有多快或多慢并不重要。所以我理解反对票。在具有尾递归支持的函数式语言中,这是一个很好的解决方案,但在 c# 中,这是一个不好的建议。
  • @reinier:一个也许吧。通过分析和/或测试轻松回答。我同意避免堆栈溢出的必要性(注意:在示例自动化中永远不会发生),但不幸的是没有其他任何事情。我看不出它是怎样的“坏建议”,因为我已经明确提到过如果存在这些问题(再次说明:示例自动机没有永远 溢出堆栈,因为没有递归)。对于 C# 和每种语言一样,可维护 解决方案是很好的建议。用gotos 编码的大型自动机?不可维护。
  • 由于性能原因,即使使用尾递归也不能使用函数。开销虽然很小,但仍然太大。无论如何,根据您的回答(最后一段),我现在更喜欢第三种选择。
【解决方案3】:

还有第四个选项。

使用迭代器来实现状态机。这是nice short article 向您展示如何

虽然它有一些缺点。无法从迭代器外部操作状态。

我也不确定它是否很快。但是你总是可以做一个测试。

【讨论】:

  • 我见过这个选项。但是太慢了。当性能很重要时,Switch 和 goto 是要走的路。
  • 我希望人们不要这样做。迭代器旨在实现迭代器;他们通过构建状态机来做到这一点是一个实现细节。当您的代码看起来像迭代器但实际上完全不同时,就很难阅读、理解、维护和调试。如果你想构建一个状态机,那就构建一个看起来像状态机的东西。
  • @Eric Lippert,为什么你如何构建状态机很重要?你用什么技术?毕竟迭代器的行为是明确定义的,有时使用框架中广泛使用的构造(如 IEnumerable)来完成某个目标肯定是有利的(IE 这也自动为您提供了 linq 作为奖金)(毕竟 IEnumerable 只不过是一个基本级别的状态机(MoveNext(); get_Current;MoveNext(); get_Current ...)
  • CCR 对迭代器也有很大的用处,便于异步编程。通常不是通过迭代器完成的事情,但它工作得很好。跳出框框思考
【解决方案4】:

切换到 goto 的优点是您可以在变量中拥有状态,而不仅仅是在指令指针中。

使用 goto 方法,状态机必须是控制其他一切的主循环,因为您无法离开它,因为您会丢失状态。

使用 switch 方法,状态机是隔离的,你可以去任何你想处理外部事件的地方。当您返回状态机时,它会从 yuu 停止的地方继续。您甚至可以同时运行多个状态机,这是 goto 版本无法实现的。

我不确定第三种选择的用途,它看起来就像第一种选择,周围有一个无用的开关。

【讨论】:

  • @Niek:嗯,现在它更有意义了。不过,我宁愿选择第二种选择。第三种选择有两种不同的状态,一种在变量中,一种在指令指针中,你很容易进入错误的状态。您可以在不更改状态变量的情况下从一种状态转到另一种状态,因此如果您离开循环并重新进入,它将返回到之前的状态。
【解决方案5】:

我个人更喜欢使用 goto 的第二个,因为第一个将需要不必要的循环步骤(例如)才能进入新状态

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-08-10
    • 2011-06-03
    • 1970-01-01
    • 2011-05-24
    • 1970-01-01
    • 2010-09-12
    • 1970-01-01
    • 2021-07-02
    相关资源
    最近更新 更多