我目前正在为状态机构建一个小型库(是的,有很多这样的库,但我很喜欢它)。现在,我遇到了一个有趣的问题。
假设给定的状态机接受某种类型的事件。如果我们要解析协议,这可能是是人物。因此,在某种状态下,字符“a”可能会导致新状态,而所有其他字符(或一组字符)仍然处于该状态(例如,接收数据包的有效负载,而没有收到ETX)。
我的状态机定义类似于Boost :: MSM的工作方式,即:一个包含触发它们的事件的转换列表:
var S1 = new BasicTState<char>("S1");
var S2 = new BasicTState<char>("S2");
var transitions = new[]
{
new TTransition<char>{from = S1, to = S2, evt = 'b', guard = null },
new TTransition<char>{from = S2, to = S1, evt = 'a', guard = null },
};
// The type parameter denotes the type of event, that is to be accepted.
// the second parameter is the initial state!
var sm = new TStateMachine<char>(transitions, S1);
sm.Event('b');
Assert.AreEqual(sm.ActiveState, S2);
到目前为止一直很好 - 现在。鉴于这种情况,我需要几个例如字符,以匹配相同的状态转换我有点卡住。天真的方式是为每个角色定义一个状态转换来接受 - 这是乏味和错误的。我想做的是使用“Matcher”对象来完成这项工作,例如:
var transitions = new[]
{
new TTransition<Matcher<char>>{from = S1, to = S2, evt = new Matcher<char>(new[] {'c', 'd'}), guard = null },
new TTransition<Matcher<char>>{from = S2, to = S1, evt = new Matcher<char>(new[] {'e', 'f'}), guard = null },
};
为了使其工作,我已经为匹配器定义了隐式转换,因此我仍然可以生成上例中的事件,即“sm.Event('c')
”。这很好用。但是现在我的情况是我的statemachine对象必须是TStateMachine<Matcher<char>>
类型,这显然很烦人,因为我现在不能使用简单的表示法,只需要匹配一个eventtype,我只能使用单一类型的匹配器,隐式类型转换将总是迫使我进入这一类型。更糟糕的是,我无法隐藏界面背后的匹配器,因为类型转换根本不会发挥作用。
我知道,我可以轻松地使用接口来解决匹配单个和多个事件类型的问题,但这会大大降低可读性(坦率地说,以这种方式解决它并不好玩)。我真正感兴趣的是,如果有任何方法可以使多态性与隐式类型转换很好地结合。我想最终会有这样的结果:
var transitions = new[]
{
new TTransition<char>{from = S1, to = S2, evt = 'b', guard = null },
new TTransition<char>{from = S2, to = S1, evt = new Matcher<char>(new[] {'e', 'f'}), guard = null },
new TTransition<char>{from = S2, to = S1, evt = new YetAnotherMatcherTypeIDontKnowAboutYet<char>(new YadaYadaObject()), guard = null }
};
答案 0 :(得分:1)
状态本身是否需要泛型类型参数?为什么国家需要知道它是如何被触发的?
我会声明像这样的转换类型
public class Transition<TEvent>
{
public BasicState From { get; set; }
public BasicState To { get; set; }
public Func<TEvent, bool> EventMatcher { get; set; }
...
}
和像这样的状态机
public class StateMachine<TEvent>
{
public StateMachine(Transition<TEvent>[] transitions, BasicState initialState)
{
...
}
...
}
并且转换将始终声明匹配器,即使在使用单个字符的简单情况下
var transitions = new[]
{
new Transition<char>{From = S1, To = S2, EventMatcher = c => c == 'b', Guard = null },
new Transition<char>{From = S2, To = S1, EventMatcher = c => c == 'a', Guard = null },
};
如果您需要匹配多个字符,可以编写
new Transition<char>{From = S2, To = S1, EventMatcher = c => "ef".Contains(c), Guard = null }
注意,我已使用Lambda Expressions(=>
语法)。
我省略了&#34; T&#34;来自类型名称的前缀,因为这在C#中并不常见(它在Pascal中很常见)。此外,属性应该在PascalCase中,即使在C#:-)中也是如此。请参阅MSDN上的Naming Guidelines。
或者,您可以声明不同类型的过渡。他们将负责匹配自己
public abstract class TransitionBase<TEvent>
where TEvent : IEquatable<TEvent>
{
public BasicState From { get; set; }
public BasicState To { get; set; }
public abstract bool DoesMatchEvent(TEvent event);
}
public class SingleEventTransition<TEvent> : TransitionBase<TEvent>
where TEvent : IEquatable<TEvent>
{
TEvent Event { get; set; }
public override bool DoesMatchEvent(TEvent event)
{
return Event.Equals(event);
}
}
public class MultiEventTransition<TEvent> : TransitionBase<TEvent>
where TEvent : IEquatable<TEvent>
{
TEvent[] Events { get; set; }
public override bool DoesMatchEvent(TEvent event)
{
foreach (TEvent e in Events) {
if (e.Equals(event)) {
return true;
}
}
return false;
}
}
请注意,由于您事先并不知道TEvent
的真实类型,因此您无法使用它,就像它是char
一样;但是,您可以指定一个泛型类型约束,要求类型是等同的,即提供Equals
方法。 char
,int
,string
等基本类型可以。如果您实现自己的事件类型,则可以自己实现IEquatable<T>
界面。
TransitionBase<char>[] transitions = new[]
{
new SingleEventTransition<char>{From = S1, To = S2, Event = 'b', Guard = null },
new MultiEventTransition<char>{From = S2, To = S1, Events = new TEvent[]{ 'e', 'f'},
Guard = null },
};