geek青年的状态机,查表,纯C语言实现

1. 问题的提出,抽象

建一,不止是他,不少人跟我讨论过这样的问题:如何才能保证在需求变更、扩充的情况下,程序的主体部分不动呢?

这是一个非常深刻和艰难的问题。在进入实质讨论之前,我们还得先明确什么是"主体",就是我们不希望动的那一部分是什么。事实上,没有什么"主体",这是被我们主观划分的,代码中有一部分是不动的,另一部分是动的。而追求永恒(一劳永逸?) ,是我们的天性吧。

我们希望实现一段程序,换一些东西,游戏就由 双截龙 变成了 超级玛丽,再换一点东西,就变成了 魂斗罗。只要招些美工,再招些脚本作者,所有的程序员就可以--解雇了。

这看起来不太现实,那么我们来看一段类似的,但是更现实一点的。我们希望实现一段程序,在每轮迭代/循环中,这段代码都能完成我们需要做的任务,虽然这些任务可能在每轮迭代中有所不同。在数学归纳法,在 sigma 符号的的周围,甚至在积分符号的周围,都在发生这样的事情。

这些梦想或者已经实现的技术,都基于"抽象"。我们试图找到在不同的情境 (动作、需求) 下那些相同的部分。我们对具体事件做抽象,并且期待抽象的结果适用于所有的具体的事例。这样,原来的很多工作就成为 应用抽象的理论 的过程,不再需要创造力,因此也不再能吸引我们。那么,我们再对抽象的结果继续抽象,直到形而上。

2. 状态机的引擎

引擎,就是上文中提到的开发出一个游戏,然后能衍生出很多游戏的技术。代码的核心部分、流程部分不会改变,只有数据 (甚至可以在外部文件中) 才随需求的变化而变化。

状态机,也可以用引擎实现。实现这一目标的技术也存在已久,就是查表。查表的经典案例是 求三角函数 (在一定精度下),常量时间复杂度的解决方案 就是查表。事先把三角函数在不同度数下的值都求出来,放在hash表 (?) 里。你要查哪个度数,我就去查哪个度数对应的函数值。

在这个案例里,查表的那段代码,不随三角函数由sin变成cos或tan而发生任何变化。这就是引擎,被查的表就是数据。

3. 接口

我们期待的接口跟前一篇普通青年中的完全一样。在主函数中调用 void state_change(enum message m) 向状态机传递消息,用 test.in 作为测试用例。主函数还知道,一共就这样几种消息:

enum message { play, stop, forward, backward, record, pause };

4. 状态迁移表

在讲如何查表前,我们先设计 表 本身。我们期待表格能够描述 状态迁移 中的要素。记得么,一共4个。 (1) 当前状态, (2)当前消息, (3)将迁移到的状态,(4)在状态迁移中的动作。我们期待能用表格,而不是如普通青年一文中用代码(switch-case)的方式描述。因为我们相信,改表格比改代码容易。

状态迁移表与状态迁移图完全等价。

表格看起来像下面这样,如果想像划上竖线效果更佳。

1 struct transition fsm[transition_num] = {
2         /* current_state, message/event, next_state*/
3     {s_play,	stop,		s_stop},
4     {s_play,	pause,		s_pause},
5     {s_pause,	pause,		s_play},
6     {s_pause,	stop,		s_stop},
7     {s_stop,	forward,	s_forward},
8     {s_stop,	play,		s_play},
9     {s_stop,	backward,	s_backward},
10     {s_stop,	record,		s_record},
11     {s_forward,	stop,		s_stop},
12     {s_backward,	stop,		s_stop},
13     {s_record,	stop,		s_stop} };

每一行,都是一组状态迁移的匹配。第一列是当前状态,第二列是接收到的消息,第三列是在此种情况下将迁移到的状态。每增加一个迁移的匹配,我们就按这样的规则增加一行。这规定了状态机迁移中4要素里的3条,剩下的那条是在迁移中的动作,后面再介绍。

当然,为了遵循C语言的语法,我们需要在此前就定义 (1) 状态枚举、 (2) 消息枚举,还有 (3) 迁移的结构体。如下。

1 enum state { s_stop=‘s‘, s_play=‘p‘, s_forward=‘f‘, s_backward=‘b‘, s_pause=‘_‘, s_record=‘r‘  };
2 enum message { play, stop, forward, backward, record, pause };
3 
4 struct transition {
5     enum state current;
6     enum message m;
7     enum state next;
8 };

我们还需要定义一共多少条迁移规则,是为了我们还没有写出来的代码准备的,不过此处已经用到,所以定义如下。

1 #define transition_num  11

5. 迁移时的动作

我们希望把迁移时的动作放在每个状态到达之处。即,每个状态都可以有一些"副作用"。这与迁移时的动作是等价的,证明略去。如果仅想在迁移时写代码,也可以利用这种方法实现。

状态机的动作 表格如下:

1 struct state_action state_action_map[state_num] = {
2     {s_stop, do_stop},
3     {s_play, do_play},
4     {s_forward, do_forward},
5     {s_backward, do_backward},
6     {s_pause, do_pause},
7     {s_record, do_record}};

每一行,是一个状态对应的动作。第一列是状态,第二列是对应的动作。这样,每增加一个状态 (如果它有对应动作),就在这里加入一行;动作对应的函数需要实现,后面会介绍。

类似于状态迁移图,为了遵循C语言语法,我们需要在此前声明如下。

1 #define state_num 6
2 typedef void (*action_foo)() ;
3 
4 enum state { s_stop=‘s‘, s_play=‘p‘, s_forward=‘f‘, s_backward=‘b‘, s_pause=‘_‘, s_record=‘r‘  };
5 
6 /* action starts */
7 void do_stop() {printf ("I am in state stop and should doing something here.\n");}
8 void do_play() {printf ("I am in state play and should doing something here.\n");}
9 void do_forward() {printf ("I am in state forward and should doing something here.\n");}
10 void do_backward() {printf ("I am in state backward and should doing something here.\n");}
11 void do_pause() {printf ("I am in state pause and should doing something here.\n");}
12 void do_record() {printf ("I am in state record and should doing something here.\n");}
13 
14 struct state_action {
15     enum state m_state;
16     action_foo foo;
17 };

第1行,是状态的数量。第2行和第7行到第12行,以及第16行,使用了函数指针(指向函数的指针,一个指针,它的基类型是一个函数),用于表示要执行的动作。第4行,是状态枚举。第14行到第17行,是 状态-动作 对应关系的结构体。

第7行至第12行,是动作的执行部分。当增加的状态需要动作时,程序员要在此处加入一个函数,它遵守第2行的签名约定。

6. 引擎

如果表格的数据结构已定,代码就好写了。我们的引擎代码的核心部分是查表,遍历表格,找到与当前状态、当前消息匹配的将迁移到的状态。

我们还是自顶向下,假设 查表部分已经完成,为主函数提供与 普通青年一文相同的接口--而内部实现是不同的。

1 void state_change(enum message m)
2 {
3     static state = s_stop;
4     enum state next;
5     int index = 0;
6 
7     index = lookup_transition(state, m, fsm);
8     if(index!=ERR)
9     {
10         state = fsm[index].next;
11         lookup_action(state, state_action_map)();
12     }
13     return;
14 }

如第3行如示,初始状态是 停止。在第7行,我们引用了一个尚未写好的函数,lookup_transition。虽然函数还不存在,不过我们能猜出来它的作用,查表,找到 当前状态是 state,当前消息是 m 时所对应的表项的下标 index。fsm参数是为了可能有多个状态迁移表设计的,此处可以略过。

当查找到 index 以后,且 index 不是 ERR (没找到),就可以令 下一个状态为state = fsm[index].next,见第10行。

以上,完成了状态迁移4要素中的3个:当前状态、当前消息、将迁移到的状态。

第11行,完成的功能是执行与状态对应的动作。这里又用到函数指针。在代码 lookup_action(state, state_action_map)() 中,lookup_action(state, state_action_map) 用于找到状态 state 对应的动作,后面的 "()",是因为这个动作是一个函数指针,可以使用这样的方式执行这个指针指向的函数。与上文中的 fsm 参数类似,state_action_map是为了应对有多个状态-动作表的情况,这里可以略过。

无论数据 (状态迁移、状态-动作)如何变化,引擎代码都不会变化。所以,甚至可以把引擎放在静态或动态链接库里,或者把数据放在外部文件里,运行时再载入,从而提高部署时的灵活性。

7. 查表

刚刚用到的两个未定义的函数 lookup_transition(state, m, fsm) 和 lookup_action(state, state_action_map) 都使用了查表的方法。

代码如下。可以看出,二者的结构非常类似,遍历数组 (for循环) ,找到符合条件的元素 (if判断),然后把该元素的索引或者该元素结构体的某个成员返回。

ERR 和 ACTION_NOT_FOUND 是用来容错的,万一表格有误,没有查到匹配的项。

1 int const ERR = -1;
2 int lookup_transition (enum state s, enum message m, struct transition * t)
3 {
4     int ret=ERR;
5     int i;
6     for(i=0;i<transition_num;++i)
7     {
8         if(t[i].current == s && t[i].m == m)
9         {
10             ret = i;
11         }
12     }
13     return ret;
14 }
15 
16 action_foo ACTION_NOT_FOUND = NULL;
17 action_foo lookup_action(enum state s, struct state_action* a)
18 {
19     action_foo ret = ACTION_NOT_FOUND;
20     int i=0;
21     for (i=0;i<state_num;++i)
22     {
23         if(s == a[i].m_state)
24         {
25             ret = a[i].foo;
26         }
27     }
28     return ret;
29 }

8. 总结

geek青年,从接口上看,与普通青年并无不同。甚至在情况相对简单 (状态少、状态迁移种类少) 的时候,代码量比普通青年还有不如。那么,geek青年的长处在哪里呢?

古人云:沧海横流方显英雄本色。古人又云:大丈夫山崩于前不变色,海啸于后不动容。

geek青年的长处在于,他始终如一,无论遇到的情形是多么糟糕多么恶劣,他始终没有变化。这个世界上,总需要一些因素,一些承诺,不随任何易变的感情、任何旁人不能承受的痛苦或诱惑而变化,稳定地坚持。这才能让我们对这个世界保留一丝希望,未来才能够和值得期待。

这一篇和上一篇的代码在这里[http://download.csdn.net/detail/younggift/7569627]。

--------------------

博客会手工同步到以下地址:

[http://giftdotyoung.blogspot.com]

[http://blog.csdn.net/younggift]

=======================

geek青年的状态机,查表,纯C语言实现,布布扣,bubuko.com

时间: 2024-08-09 13:49:24

geek青年的状态机,查表,纯C语言实现的相关文章

普通青年的状态机,纯C语言

我们第一次接触到状态机,是在数字电路课程里.计数器.串行奇偶检校.检验三个1连续出现的报错电路 等,都需要状态机作为模型.实现这些功能的电路,与状态机的状态转换图.状态转换表都是等价的. 后来,我们再接触状态机,是在编译原理课程里.状态机用于描述与正则表达式匹配的字符串. 再后来,我们在GUI界面设计中,需要设置一些控件在某些条件下 禁用,某些条件下使能,某些条件下打个对号.这也可以用状态机模型来控制. 1. 不要写成 消息响应/事件处理 状态机和消息响应都是 双层 switch-case 结构

这可能是AI、机器学习和大数据领域覆盖最全的一份速查表

https://mp.weixin.qq.com/s?__biz=MjM5ODE1NDYyMA==&mid=2653390110&idx=1&sn=b3e5d6e946b719d08b67d9ebf88283fe&chksm=bd1c3d0d8a6bb41bf05a8ccc9f375528c7c5e4223b190acc9593082b50e17855d2ccdd0e8ac2&mpshare=1&scene=23&srcid=0110mg1nBdOA

社保系列10——返回值速查表

9000 命令执行成功 6006 依据传输模式,所要读取的字节长度错 61xx 正常处理.'xx'表示可以通过后续 GET RESPONSE命令得到的额外数据长度 6281 回送数据可能出错 6282 文件长度<Le 6283 选择文件无效 6284 FCI格式与P2指定的不符 6300 认证失败 63Cx 验证失败,x =0 表示不提供计数器 x !=0 表示重试次数 6581 EEPROM损坏,导致卡锁定 6700 Lc或Le长度错 6900 无信息提供 6901 命令不接受(无效状态) 6

SQL自连接(源于推荐算法中的反查表问题)

"基于用户的协同过滤算法"是推荐算法的一种,这类算法强调的是:把和你有相似爱好的其他的用户的物品推荐给你. 要实现该推荐算法,就需要计算和你有交集的用户,这就要用到物品到用户的反查表. 先举个例子说明下反查表:甲喜欢的物品有:A.B.C:乙喜欢的物品有:B.E.F:丙喜欢的物品有:A.J.K:而你喜欢的物品是:A.J.M.反查表就是喜欢A物品的有你.甲.丙,喜欢J物品的有你.丙,喜欢M物品的只有你,这就是和你喜欢的物品有联系的用户.有了这个反查表,我们就可以看出和你有关系的用户只有甲和

【转】游戏程序员的数学食粮05——向量速查表

原文:http://gad.qq.com/program/translateview/7172922 翻译:王成林(麦克斯韦的麦斯威尔)  审校:黄秀美(厚德载物) 这是本系列大家盼望已久的第五篇.如果你对向量了解不多,请先查看本系列的前四篇文章:介绍,向量基础,向量的几何表示,向量的运算. 这篇速查表会列举一些游戏中常见的几何问题,以及使用数学向量解决它们的方法. 基本向量运算的完整表单 首先,先复习一下. 首先我假设你有一个可用的向量类.它的功能大部分集中在2D上,但是3D的原理相同.差别只

模糊PID控温算法的具体实现(二):MSP430F5438A怎么实现查表法

工程上要实现参数自整定模糊PID算法,最常采用的方法是查表法.具体实现方法是将不同的E(温度误差),EC(误差变化率)与 △Kp, △Ki , △Kd的规则制成一张表格存储在单片机内部.那么在每一采样得到的温度数据模糊化得到E和Ec后,便可以通过查表从而得到相应的△Kp,△Ki和△Kd了.这个表类似于下面的形式: 那么怎么在MSP430F5438A中插入这种类似的表呢,是插在Flash里面还是SRAM里面呢?

十进制转换成其它进制的通用写法(查表法)

 //十进制转换成其它进制的通用写法(查表法)  class Transform  { public static void main(String[] args) {    toHex3(60);    System.out.println();    toOctal2(20);    System.out.println();    toBinary2(6); }  public static void toAny(int num,int base,int offSet)  {   char

STL容器用法速查表:list,vector,stack,queue,deque,priority_queue,set,map

STL容器用法速查表:list,vector,stack,queue,deque,priority_queue,set,map   list vector deque stack queue priority_queue set [unordered_set] map [unordered_map] multimap [unordered_multimap]     contiguous storage double-ended queue LIFO FIFO 1st is greatest  

三角函数查表法和三角函数值数组生成方法

今天打算用STM32驱动TFTLCD屏显示显示一个画扇形的程序,这样就需要我们有一个画圆弧的程序,我尝试了很多方法,其中有一种方法就是使用三角函数来确定圆弧的点的坐标,即: x=radius*cos(angle); y=radius*sin(angle); 下面是当时的计算过程:我们先把角度变成弧度,我们在这里使用了ToRad()函数来实现,程序十分简单. #include <math.h> #define pi 3.141592653f int shu; float ToRad(float