20140731

如果遇到实验室事故

如果遇到实验室事故

作为一个原来学计算机和电子的学生,我跟化学有千丝万缕的联系,所以亲眼和耳
闻了一些事故。

我亲眼见过的事故一共也就只有一次,影响范围不过几米。中午跟导师正谈着,传
来崩地一声。我们俩人同时冲到门口,我离门还近一些。犹豫了一 下,我说:你先。

这是个经典的笑话,一般讲到这里大家就哈哈大笑了。不过,故事的真正寓意并不
是我这个学生在危险当前的时候把导师推前面去了。在事故和危险面 前,应该服
从专家,并且现场只应产生一个专家。导师是专家,因此他应该走在前面。

那次是有人糊涂,倒残液的时候把酸液倒到碱液的瓶子里了。反应生成热和气体,
积累到中午没人的时候终于炸开瓶子。瓶子稀碎,门上玻璃裂了一 块。旁边一个
人也没有,这是万幸。

有的事故中,不服从专家后果就严重多了。在网上看来的一个事故。似乎是浓硫酸
喷了,在车间里。负责的小伙喊大家把衣服全脱了,马上淋水。被喷 的人里有女
同志害羞,跑到卫生间去了。这点路程导致的结果很严重,我忘记了是深度烧伤还
是把命搭进去了。

学生在"下实验室"以前,有的会问"要注意些啥啊",有的胆大就啥也不问。照例,
我要重复一遍我导师当初对我说的:什么也不要碰。

"什么也不要碰",有个反面例子。关同学曾经编程的时候,没事就摆弄一个玻璃
管。包师弟有一次语气淡然,"小关,你知道那玻璃管里装过什么 吗?"十有八九
是氰化物。学计算机的思维是,他们怎么能这么干呢,这玩意怎么能到处乱扔呢?
问题是,世界就是这个样的,他们乱扔以后还能为后 果负责呢,就像闯红灯的司
机,但是痛苦还是你来承担。

"什么也不要碰",遵循这一原则,我在用氮气吹灰的时候都是请别的同学帮我开气
阀。有不止一个同学跟我说过,这个玩意用起来特别简单,这么一 拧,再那么一
拧,你自己也能整。我每次都老老实实地请求,你替我整吧,而且别走,一会再替
我关上。有同学问过,这有什么难的。我的回答是,最 可怕的是,万一有一天我
觉得自己学会了,把纯氧的气阀当氮气拧开了可咋整。纯氧气体喷在柴火上,能直
接起火,还可能导致爆炸。而我,没有判别 氧气和氮气的能力。有同学可能说
了,那有什么难的,根据气瓶的颜色不就行吗?说起来简单,我见过绿瓶子,按说
应该装氯气的,有的组在里面装了 氮气--这也许没啥,不过既然有这种糊涂的
人,天知道他们不会把氯气装氮气瓶子里。

气体装反这种低级错误可能让人认为根本不会发生吧。不过我导师就是活生生的受
害者。他每到冬天的时候就卡卡咳,跟个小老头似的。那是因为他年 轻的时候做
实验,正做着,有人顺手把通风厨关了,然后导师的呼吸道就被酸性气体腐蚀了。
医生说,永久不能治愈,只能缓解。所以说,猪一样的队 友总是存在的,小心在
意这种事,要由自己负责。不懂的东西,或者不确定懂的东西,啥也别碰是第一原则。

不仅别碰任何东西,如果旁边人做实验,最好也离远点。某组有个师姐,被旁边师
弟的氨水溅在脸上一滴。大多数同学学过初高中化学,对氨水就只知 道气味难
闻,就是夏天厕所那味,可能并不了解它具有腐蚀性。幸亏溅到的氨水不多,而且
在下巴上。其实,下巴离眼睛又能有多远呢。

这是看得见的危险,真正的危险是你看不到的,特别是你以为你能看到。福尔摩斯
说,人只能看到自己想看到的东西。当年想在棱镜上刻标记,想到了 氢氟酸。高
中化学学的,氢氟酸能腐蚀玻璃。我跟导师说,怎么在玻璃上整上蜡,然后怎么拿
小刀划个印,怎么把氢氟酸滴在印上,或者把玻璃泡在氢 氟酸里。导师听完想了
一会儿,说,那个谁某某某,你去整吧,小杨不行。我还不服呐,心想,氢氟酸不
是弱酸吗,这有啥?多年以后,读到网上那个 著名的化学事故大集锦的贴子,上
面说,氢氟酸会透过皮肤进入人体,取代骨中的钙,而且到处游走,因为化学性质
如此活泼,因此无法清除。那块皮 肤永远都是烂了的样子,永远也治不好。医学
之发达,令很多人错觉这种小病不算啥,这个错觉忽视了两个事实,一是这种病医
学也很少见到,二是浓 度不同,化学性质差别可能极大。而咱们平时的生活中,
根本见不到高浓度的危险品,因此可能觉得实验和生产上也是这样。

实验和生产上完全不一样,因此遵循指令和原则非常重要,可能性命相关。一次去
钢厂,被要求戴安全帽,穿长袖工作服。钢厂里那个热啊,烤得慌。 同行有个小
伙汗流浃背的,就把安全帽摘了,马上遭到同行大哥的呵斥。该小伙后来把长袖衣
服脱了,从车间出来以后,露出来的胳膊像在海边晒过半 个月,感觉毛都掉光
了。我这才明白为什么非得要求穿长袖。鉴于学计算机的同学没有遇到过啥危险,
胆子大,我后来强烈建议上级,这个项目咱们还 是别做吧。

刚刚听到某师弟说,他的一个同学的典故。500L什么釜蒸馏减压爆了,把这同学伤
了。那天,他破天荒穿了防爆服和面具,防爆服被炸得一条条 的,胸口和胳膊也
被玻璃划了。如果没有防护,后果可想而知。

成天做实验,难保没有某天头脑发昏的时候,所以总是按原则、规章来做就安全多
了。导师还讲过吉大当年有个哥们就有头脑不清的时候,高压瓶上的 气阀滞住
了,把气瓶放倒了蹲前面拿扳子砸。怎么砸也觉得不对劲,后来就骑气瓶上砸。砸
着砸着气阀飞出去,嵌对面墙里了。如果当时他还蹲前面, 估计就被气阀打穿了。

就是万分小心,自己也有发昏到连规章都忘了的时候,就是万分小心,也有队友变
成猪连累自己的时候。如果一旦遇到实验室事故,导师对此也有指导 原则。导师
说,如果看到着火了,你什么也不要管,不要想着救同学救设备救资料的,什么也
没有人值钱;如果着了,一看灭不掉,跑。

所以,我记住了实验室安全两条原则:啥也别碰;遇到着火,跑。

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

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

[http://giftdotyoung.blogspot.com]

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

20140629

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

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]
=======================

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

我们第一次接触到状态机,是在数字电路课程里。计数器、串行奇偶检校、 检验
三个1连续出现的报错电路 等,都需要状态机作为模型。实现这些功能的电路,与
状态机的状态转换图、状态转换表都是等价的。

后来,我们再接触状态机,是在编译原理课程里。状态机用于描述与正则表达式匹
配的字符串。

再后来,我们在GUI界面设计中,需要设置一些控件在某些条件下 禁用,某些条件
下使能,某些条件下打个对号。这也可以用状态机模型来控制。

1. 不要写成 消息响应/事件处理

状态机和消息响应都是 双层 switch-case 结构。不同的是,状态机的外层是状
态,内层是消息;消息响应外层是消息,内层是状态。

有的同学会说,那又有多大的区别呢?代码只是外在形式而非本质,它所反应的是
你对模型的理解,或者说,对于问题,你使用了哪种模型。

消息响应适合于这样的情形:有很多种消息,对于同一种消息,你的程序总是给出
同一种反应。打个比方,你女朋友喜欢吃冰淇淋,任何时候你给她 买,她都高
兴,或者转怒为喜,或者转悲为喜,总之,会置心情为"喜"。这种情形,适合用消
息响应解决。

而状态机适合于另一种情形,你的程序是"有状态的",它在不同的情况 (状态)
下,会对同一消息做出不同的反应。状态,是一种数据,但是它影响流程的行为。
按面向对象的观点,数据与流程间的这种高内聚关系,非常适合用 类 来实现。这
是题外话,我们回到女朋友和冰淇淋间的关系。你女朋友可能并非在任何情况下吃
了冰淇淋都高兴,比如刚刚吃完十个八个的时候...这与她当前的状 态有关。

状态机中,我们需要掌握的核心的数据是:当前状态,当前消息,将迁移到的状
态,在迁移中发生的动作。

在状态机代码之前,请先看一段消息响应机制,VC生成的win32api代码大抵如此。
我们随便找来一段片断看看:

1 LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam,
LPARAM lParam)
2 {
3 int wmId, wmEvent;
4 PAINTSTRUCT ps;
5 HDC hdc;
6 switch (message)
7 {
8 case WM_COMMAND:
9 wmId = LOWORD(wParam);
10 wmEvent = HIWORD(wParam);
11 case ID_MENU_GO: .... break;
12 case IDM_ABOUT: .... break;
13 case IDM_EXIT: .... break;
14 default:
15 return DefWindowProc(hWnd, message, wParam, lParam);
16 }
17 break;
18 case WM_PAINT: .... break;
19 case WM_DESTROY: ... break;
20 case WM_KEYDOWN: ... break;
21 default: return DefWindowProc(hWnd, message, wParam, lParam);
22 }
23 return 0;
24 }

第6行开始到第22行结束,对每个消息给出一个响应。没错,win32api也把这个传
进来的东西称为 message。这是很典型的适合消息响应机制的情形,程序对于相同
的消息,处理的方法总是相同的。

我们常常错误地把状态机写成了消息响应,消息这部分处理得不错,但是,由于没
有很好地记录和迁移状态,写起来容易把自己写糊涂了。无他,用错 了工具。拿
螺丝刀打孔,不是工具差,而是工程师选错了工具。

2. 状态机实例,录音机

实例得是相对简单的,不然我们很容易淹没在细节之中,没有足够精力去关注状态
机本身的机制了。假设我们仿真一台录音机...

我们先假设你见过录音机。录音机是一种曾经先进的设备,有一个或两个"卡",可
以放进磁带。"卡"前面有几个按键,这几个按键上的标识因为图 形简单且示意性
强,现在还在广泛使用。它们分别是 播放 > 、暂停 || 、快进 >> 、快退<< 、
录音 O 、停止 []。

这几个按键之间是有一定的"互斥关系"的。比如当播放键按下时,我们不应该能把
快进键按下。当然,淘气的同学可能这样干过,我们会听到"咔咔"的声音,然后是
家长骂败家玩艺的声音。可以就"互斥关系"开始写程序,但是我觉得这样有点 麻烦。

我们认为,这种"互斥关系"是因为录音机是"有状态的"。所以,我们打算用状态机
来实现。状态转换图是这样的。请读图的时候关注这四点:当前 状态,当前消
息,将迁移到的状态,在迁移中发生的动作 (本例中没有) 。

digraph state
{
graph [ nodesep=1.2];
rankdir = LR;

播放 -> 暂停 [label="按下 || "];
暂停 -> 播放 [label="按下 || "];
暂停 -> 停止 [label="按下 []"];
停止 -> 播放 [label="按下 >"];
播放 -> 停止 [label="按下 []"];
停止 -> 快退 [label="按下 <<"];
停止 -> 快进 [label="按下 >>"];
快进 -> 停止 [label="按下 []"];
快退 -> 停止 [label="按下 []"];
停止 -> 录音 [label="按下 O"];
录音 -> 停止 [label="按下 []"];
}

备注:我实在想不起来 暂停 和 停止 之间的关系了,似乎是这样的,又似乎不
是。反正大概是那么个意思,不影响对状态机的理解,就这么地吧。

接下来是C代码实现。

3. 接口 及 测试

看到以下代码,有的同学会说,你这不就是主程序么,为什么要把小标题叫做接
口。因为,它规定了我们的状态机函数将是什么样子的。

1 enum message { play, stop, forward, backward, record, pause };
2
3 int main(int argc, char *argv[])
4 {
5 char c=0x00;
6 while(1)
7 {
8 c = getchar();
9 switch(c)
10 {
11 case ' ': state_change(pause); break;
12 case 'p': state_change(play); break;
13 case 'r': state_change(record); break;
14 case 's': state_change(stop); break;
15 case 'f': state_change(forward); break;
16 case 'b': state_change(backward); break;
17 case 'q': return EXIT_SUCCESS;
18 }
19 }
20 return EXIT_SUCCESS;
21 }

上述代码规定了,状态机迁移函数的原型/签名是 void state_change(enum
message m)。

测试的时候,我们这样做:./state < test.in。test.in的内容是"psfsbspq",测
试时期待看到输出的状态迁移过程。之所以这样做,而不是每次从控制台手动输
入,是因为每 次测试的内容都应该是相同的--相同的输入,程序有相同的反应--
可重现性。或者说,DRY原则。

一个非常值得我们注意的问题。在上述接口中,我们看不到"状态"。事实上,我们
将会定义:

enum state { s_stop, s_play, s_forward, s_backward, s_pause, s_record };

但是,接口以外的代码,是 *不应该* (是不应该,不是 不必要,是一定不要) 知
道状态的,既不应该知道当前状态,也不应该知道将要迁移到哪个状态,也不应该
知道在迁移过程中应该做什么动作。如果接口以外的代码知道了这些,就侵入了
状态机的隐私,子系统的边界就模糊了。而契约的首要任务就是规定边界,规定国
家与个人、个人与个人、个人与集体的边界。

这一原则,早在195X年,软件工程刚刚开始的时候就确立了,是最初确立的原则,
即 信息隐藏。后面的原则,都是它的儿子孙子。有个比喻讲过这个道理。当你在
超市出口付款的时候,你会自己把钱从钱夹里拿出来递给售货员,而不会转过身去
对她 说,"在我屁股兜里,你自己掏吧,别忘了把零钱放回来。"这既增加了假设
--你极端信任她,也增加了她的责任。

接口,最主要的任务就是为了明确责任,把责任分布在子系统边界两侧。其次才是
规定调用的方法,即边界长什么样。

4. 状态迁移

以下是状态机的代码片断。

1 enum state { s_stop, s_play, s_forward, s_backward, s_pause, s_record};
2 void state_change(enum message m)
3 {
4 static enum state s=s_stop;
5 switch (s)
6 {
7 case s_play:
8 if(m==stop)
9 {
10 s = s_stop;
11 printf("stop.\n");
12 }
13 else if (m==pause)
14 {
15 s = s_pause;
16 printf("pause");
17 }
18 break;

我们还是要关注那四个关键点: (1) 当前状态, (2) 当前消息, (3) 将迁移到
哪个状态, (4) 迁移中会做哪些动作。

(1) 当前状态必然是第1行的枚举类型中的一个。我们初始化状态为 停止,见第4行。

在第5行到第7行,我们的双重 switch-case 的外层 按当前状态分类,如下。
5 switch (s)
6 {
7 case s_play:
下面还有很多 case,第1行的枚举类型中的每一个状态,都有一个 case。

(2) 当前消息。如果当前状态是第7行了,那么,当前消息由双层 switch-case的
内层,即第8行,第13行的 if...else if 来响应。

(3) 将迁移到哪个状态。在 s_play状态 (第7行) 接收到 stop 消息 (第8行)的
话,将迁移到 s_stop 状态,即第10行。

(4) 在迁移中会做哪些动作,如果还是这个状态这个消息,会做的动作是 第11
行,打印一段文字描述接下来的状态。

在函数 void state_change(enum message m) 中,维护了当前状态,规定了在某
种状态下-接收到某个消息,会迁移到哪个状态,在状态迁移中做哪些动作。

主函数在调用state_change时,是通过这一接口,向状态机发送一个消息;由状态
机对这个消息做出适合自己当前状态的响应--状态迁 移、动作。主函数所看到
的,是一个多彩或善变的女人,而她之所以对同一消息做出不同响应的原因,在她
的内心深入保留着,那是她不会对你说的状 态,以及状态迁移中的波澜壮阔。即
使表面上善变的状态机,也是可以理解和预测的,如果她对你倘开心扉,允许你一
行一行把附录A中的代码读完, 了解所有的 switch-case,了解所有的状态下她将
会如何响应每一种消息。

附录A 完整代码

1 #include <stdlib.h>
2 #include <stdio.h>
3
4
5 //recorder
6
7 enum state { s_stop, s_play, s_forward, s_backward, s_pause, s_record };
8 enum message { play, stop, forward, backward, record, pause };
9
10
11 void state_change(enum message m)
12 {
13 static enum state s=s_stop;
14 switch (s)
15 {
16 case s_play:
17 if(m==stop)
18 {
19 s = s_stop;
20 printf("stop.\n");
21 }
22 else if (m==pause)
23 {
24 s = s_pause;
25 printf("pause");
26 }
27 break;
28 case s_pause:
29 if(m==pause)
30 {
31 s = s_play;
32 printf("play.\n");
33 }
34 else if(m==stop)
35 {
36 s = s_stop;
37 printf("stop.\n");
38 }
39 break;
40 case s_stop:
41 if(m==play)
42 {
43 s = s_play;
44 printf("play.\n");
45 }
46 if(m==backward)
47 {
48 s = s_backward;
49 printf("backward.\n");
50 }
51 if(m==forward)
52 {
53 s = s_forward;
54 printf("forward.\n");
55 }
56 if(m==record)
57 {
58 s = s_record;
59 printf("record.\n");
60 }
61 break;
62 case s_forward:
63 if(m==stop)
64 {
65 s = s_stop;
66 printf("stop.\n");
67 }
68 break;
69 case s_backward:
70 if(m==stop)
71 {
72 s = s_stop;
73 printf("stop.\n");
74 }
75 break;
76 case s_record:
77 if(m==stop)
78 {
79 s = s_stop;
80 printf("stop.\n");
81 }
82 break;
83
84
85 }
86
87 }
88
89
90 int main(int argc, char *argv[])
91 {
92 char c=0x00;
93 while(1)
94 {
95 c = getchar();
96 switch(c)
97 {
98 case ' ': state_change(pause); break;
99 case 'p': state_change(play); break;
100 case 'r': state_change(record); break;
101 case 's': state_change(stop); break;
102 case 'f': state_change(forward); break;
103 case 'b': state_change(backward); break;
104 case 'q': return EXIT_SUCCESS;
105 }
106
107
108 }
109
110 return EXIT_SUCCESS;
111 }

附录B 状态图源代码 in graphviz

digraph state
{
graph [ nodesep=1.2];
rankdir = LR;

播放 -> 暂停 [label="按下 || "];
暂停 -> 播放 [label="按下 || "];
暂停 -> 停止 [label="按下 []"];
停止 -> 播放 [label="按下 >"];
播放 -> 停止 [label="按下 []"];
停止 -> 快退 [label="按下 <<"];
停止 -> 快进 [label="按下 >>"];
快进 -> 停止 [label="按下 []"];
快退 -> 停止 [label="按下 []"];
停止 -> 录音 [label="按下 O"];
录音 -> 停止 [label="按下 []"];

}



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

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

[http://giftdotyoung.blogspot.com]

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