一个状态机的实现
话不多说,先看代码:
interface istate { string name { get; set; } //后件处理 ilist<istate> nexts { get; set; } func<istate /*this*/, istate /*next*/> selector { get; set; } } class state : istate { public string name { get; set; } = "state"; ilist<istate> istate.nexts { get; set; } = new list<istate>(); public func<istate, istate> selector { get; set; } }
状态比较简单,一个name标识,一个后件状态列表,然后一个状态选择器。
比如状态a,可以转移到状态b,c,d,那么选择器就是其中一个。至于怎么选,就让用户来定义实际的选择器了。
delegate bool handletype<t>(istate current, istate previous,ref t value); interface icontext<t> : ienumerator<t>, ienumerable<t> { //data t value { get; set; } //前件处理 idictionary<tuple<istate/*this*/, istate/*previous*/>, handletype<t>> handles { get; set; } istate currentstate { get; set; } bool transition(istate next); }
和状态类state关注后件状态不同,上下文类context关注前件状态。当跳转到一个新的状态,这个过程中就要根据当前状态来实施不同的策略。比如想进入状态c,根据当前状态是a, b,d 有不同的处理程序。这种转移处理程序,是一一对应的,所以用了 tuple<进入的状态,当前状态> 来描述一个跳转链。然后用dictionary 捆绑相关的处理程序。
上下文会携带 t value 数据,要怎么处理这种数据?我是通过ref 参数来传递给处理程序。因为我不想istate 关心上下文的构造,它只需要关注实际的数据 t value;
上下文保存数据和当前状态,然后通过transiton 让用户控制状态的转移。这里面有一个重复,因为istate有选择器来控制状态转移了。为什么要这么处理?我是为了构造一个跳转序列。引入ienumerator和ienumerable接口,然状态可以在选择器的作用下自动跳转,然后用foreach 读取结果序列(只是不知道有什么用)。
class context<t> : icontext<t> { t data; t icontext<t>.value { get=>data ; set=>data = value; } idictionary<tuple<istate, istate>, handletype<t>> icontext<t>.handles { get; set; } = new dictionary<tuple<istate, istate>, handletype<t>>(); public istate currentstate { get; set;} t ienumerator<t>.current => (this as icontext<t>).value ; object ienumerator.current => (this as icontext<t>).value; bool icontext<t>.transition(istate next) { icontext<t> context= this as icontext<t>; if (context.currentstate == null || context.currentstate.nexts.contains(next)) { //前件处理 var key = tuple.create(next, context.currentstate); if (context.handles.containskey(key) && context.handles[key] !=null) if (!context.handles[key](next, context.currentstate,ref this.data)) return false; context.currentstate = next; return true; } return false; } bool ienumerator.movenext() { //后件处理 icontext<t> context = this as icontext<t>; istate current = context.currentstate; if (current == null) throw new exception("必须设置初始状态"); if (context.currentstate.selector != null) { istate next= context.currentstate.selector(context.currentstate); return context.transition(next); } return false; } void ienumerator.reset() { throw new notimplementedexception(); } #region idisposable support private bool disposedvalue = false; // 要检测冗余调用 protected virtual void dispose(bool disposing) { if (!disposedvalue) { if (disposing) { // todo: 释放托管状态(托管对象)。 } // todo: 释放未托管的资源(未托管的对象)并在以下内容中替代终结器。 // todo: 将大型字段设置为 null。 disposedvalue = true; } } // todo: 仅当以上 dispose(bool disposing) 拥有用于释放未托管资源的代码时才替代终结器。 // ~context() { // // 请勿更改此代码。将清理代码放入以上 dispose(bool disposing) 中。 // dispose(false); // } // 添加此代码以正确实现可处置模式。 void idisposable.dispose() { // 请勿更改此代码。将清理代码放入以上 dispose(bool disposing) 中。 dispose(true); // todo: 如果在以上内容中替代了终结器,则取消注释以下行。 // gc.suppressfinalize(this); } ienumerator<t> ienumerable<t>.getenumerator() { return this; } ienumerator ienumerable.getenumerator() { return this; } #endregion }
重点关注transition函数和movenext函数。
bool icontext<t>.transition(istate next) { icontext<t> context= this as icontext<t>; if (context.currentstate == null || context.currentstate.nexts.contains(next)) { //前件处理 var key = tuple.create(next, context.currentstate); if (context.handles.containskey(key) && context.handles[key] !=null) if (!context.handles[key](next, context.currentstate,ref this.data)) return false; context.currentstate = next; return true; } return false; }
做的事也很简单,就是调用前件处理程序,处理成功就转移状态,否则退出。
bool ienumerator.movenext() { //后件处理 icontext<t> context = this as icontext<t>; istate current = context.currentstate; if (current == null) throw new exception("必须设置初始状态"); if (context.currentstate.selector != null) { istate next= context.currentstate.selector(context.currentstate); return context.transition(next); } return false; }
movenext通过选择器来选择下一个状态。
总的来说,我这个状态机的实现只是一个框架,没有什么功能,但是我感觉是比较容易编写状态转移目录树的。
用户首先要创建一组状态,然后建立目录树结构。我的实现比较粗糙,因为用户要分别构建目录树,前件处理器,还有后件选择器这三个部分。编写测试代码的时候,我写了9个状态的网状结构,结果有点眼花缭乱。要是能统一起来估计会更好一些。
要关注的是第一个状态,和最后的状态的构造,否则无法停机,嵌入死循环。
//测试代码 //---------创建部分--------- string mess = "";//3 istate s3 = new state() { name = "s3" }; //2 istate s2 = new state() { name = "s2" }; //1 istate s1 = new state() { name = "s1" }; //---------组合起来--------- s1.nexts = new list<istate> { s2, s3 }; s2.nexts = new list<istate> { s1, s3 }; s3.nexts = new list<istate> { }; //注意end写法 //---------上下文--------- //transition icontext<int> cont = new context<int> { currentstate=s1};//begin cont.value = 0; //---------状态处理器--------- handletype<int> funclaji = (istate current, istate previous, ref int v) => { mess += $"{current.name}:垃圾{previous.name}\n"; v++; return true; }; //1 cont.handles.add(tuple.create(s1 , default(istate)), funclaji); cont.handles.add(tuple.create(s1, s2), funclaji); //2 cont.handles.add(tuple.create(s2, s1), funclaji); //3 cont.handles.add(tuple.create(s3, s1), funclaji); cont.handles.add(tuple.create(s3, s2), funclaji); //---------状态选择器--------- var rval = new random(); func<int,int> round = x => rval.next(x); s1.selector = st => round(2)==0? s2:s3; s2.selector = st => round(2)==0? s1:s3;
构造完毕后,就可以使用这个状态机了。
//选择器跳转 mess += "选择器跳转:\n------------------------\n"; foreach (var stor in cont) mess+=$"状态转变次数:{stor}\n"; //直接控制跳转 mess += "\n直接控制状态跳转:\n------------------------\n"; cont.transition(s1); cont.transition(s2); cont.transition(s3);
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持!