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)的方式描写叙述。由于我们相信,改表格比改代码easy。

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

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveW91bmdnaWZ0/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" width="350" height="308" alt="">

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

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

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

4 enum state { s_stop='s', s_play='p', s_forward='f', s_backward='b', s_pause='_', s_record='r'  };

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;

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语言实现的更多相关文章

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

    https://mp.weixin.qq.com/s?__biz=MjM5ODE1NDYyMA==&mid=2653390110&idx=1&sn=b3e5d6e946b719 ...

  2. 简明 Git 命令速查表(中文版)

    原文引用地址:https://github.com/flyhigher139/Git-Cheat-Sheet/blob/master/Git%20Cheat%20Sheet-Zh.md在Github上 ...

  3. .htaccess下Flags速查表

    Flags是可选参数,当有多个标志同时出现时,彼此间以逗号分隔. 速查表: RewirteRule 标记 含义 描述 R Redirect 发出一个HTTP重定向 F Forbidden 禁止对URL ...

  4. YUV420查表法高效、无失真的转换为RGB32格式

    YUV格式有两大类:planar和packed.planar的YUV格式,先连续存储所有像素点的Y,紧接着存储所有像素点的U,随后是所有像素点的V,这里所讲述的就是这中存储格式的:packed的YUV ...

  5. C#,Java,C -循环冗余检验:CRC-16-CCITT查表法

    C#代码 using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ...

  6. atitit.设计模式(2) -----查表模式/ command 总结

    atitit.设计模式(2) -----查表模式/ command 总结 1. 应用场景: 1 1. 取代一瓦if else 1 2. 建设api rpc风格的时候儿. 1 3. 菜单是Command ...

  7. Markdown 语法速查表

      Markdown 语法速查表 1 标题与文字格式 标题 # 这是 H1 <一级标题> ## 这是 H2 <二级标题> ###### 这是 H6 <六级标题> 文 ...

  8. C语言:十进制进制转换为其他进制(思想:查表法)

    // //  main.c //  Hex conversion // //  Created by ma c on 15/7/22. //  Copyright (c) 2015年 bjsxt. A ...

  9. jQuery API 3.1.0 速查表-打印版

    jQuery API 3.1.0 速查表-打印图,(API来自:http://jquery.cuishifeng.cn/index.html)

随机推荐

  1. IE 浏览器在地址栏输入中文字符,发送get请求报400错误的问题

    因为学校有JavaWeb的课程,所以才接触这方面.最近遇到了个小问题. 先看一段很简单的jsp代码例子 <%@ page language="java" import=&qu ...

  2. Git——基本操作

    Shell 基本概念 shell俗称壳,为使用者提供使用界面,例如DOS下command以及后来的cmd.exe shell分类 图形界面shell,提供友好的可视化界面,例如windows操作界面, ...

  3. 基于C++的多态性动态判断函数

    这里先有一个问题: 问题描述:函数int getVertexCount(Shape * b)计算b的顶点数目,若b指向Shape类型,返回值为0:若b指向Triangle类型,返回值为3:若b指向Re ...

  4. [转]Js获取当前日期时间及其它操作

    转载自:http://www.cnblogs.com/carekee/articles/1678041.html Js获取当前日期时间及其它操作 var myDate = new Date();myD ...

  5. Java基础——异常

    一.什么是异常  异常的英文单词是exception,字面翻译就是“意外.例外”的意思,也就是非正常情况.事实上,异常本质上是程序上的错误,包括程序逻辑错误和系统错误.比如使用空的引用.数组下标越界. ...

  6. LeetCode15——3Sum

    数组中找三个数和为0的结果集 1 // 解法一:先排序 然后固定一个值 然后用求两个数的和的方式 public static List<List<Integer>> three ...

  7. css--小白入门篇5

    一.行高和字号 1.1 行高 CSS中,所有的行,都有行高.盒模型的padding,绝对不是直接作用在文字上的,而是作用在“行”上的. 1 line-height: 40px; 文字,是在自己的行里面 ...

  8. 51nod 1118 机器人走方格【dp】

    M * N的方格,一个机器人从左上走到右下,只能向右或向下走.有多少种不同的走法?由于方法数量可能很大,只需要输出Mod 10^9 + 7的结果. 收起 输入 第1行,2个数M,N,中间用空格隔开.( ...

  9. Mybatis中and和or的细节处理

    当一条SQL中既有条件查又有模糊查的时候,偶尔会遇到这样的and拼接问题.参考如下代码: <select id="listSelectAllBusiness"> sel ...

  10. SQL Server数据库基础编程

    转载,查看原文 Ø Go批处理语句 用于同时执行多个语句 Ø 使用.切换数据库 use master go   Ø 创建.删除数据库 方法1. --判断是否存在该数据库,存在就删除 if (exist ...