这个编程技巧别说我没告诉你

  • 2019 年 11 月 25 日
  • 筆記

来源:公众号【编程珠玑】

作者:守望先生

ID:shouwangxiansheng

前言

有读者在后台留言说用c写一篇有限状态机的推文,正好之前也用过,就分享一下吧。

背景

先举一个简单的例子,假设是这样的,一个小孩有两种状态,睡眠,清醒。睡的时候可能会撒尿,微笑,撒尿之后会转为清醒状态,而清醒的时候可能会笑,会吃,吃完之后会转会睡眠状态 用C语言实现,一般写法可能是这样的:

//来源:公众号【编程珠玑】  #include <stdio.h>  enum KID_STATUS  {      SLEEP = 0,      WAKE = 1,      INVALID = 2  };  enum KID_ACTION  {      SMILE = 0,      EAT = 1,      PEE = 2  };  /*孩子当前状态*/  static int nowStatus = WAKE;    /*孩子行为*/  int smile(int status)  {      printf("kid smilen");      return status;  }  int eat(int status)  {      printf("kid eatn");      return SLEEP;  }  int pee(int status)  {      printf("kid peen");      return WAKE;  }  /*孩子产生某个行为*/  void execute(int action)  {      int nextStatus;      /*醒着时的行为*/      if(WAKE == nowStatus)      {          switch(action)          {              case SMILE:              {                  nextStatus = smile(nowStatus);                  break;              }              case EAT:              {                  nextStatus = eat(nowStatus);                  break;              }              case PEE:              {                  nextStatus = pee(nowStatus);                  break;              }              default:                  printf("unknow action when wake %dn",action);                  nextStatus = nowStatus;                  break;          }      }      /*睡着时的行为*/      else if(SLEEP == nowStatus)      {          switch(action)          {              case SMILE:              {                  nextStatus = smile(nowStatus);                  break;              }              casePEE:              {                  nextStatus = pee(nowStatus);                  break;              }              default:                  printf("unknow action when sleep %dn",action);                  nextStatus = nowStatus;                  break;          }      }      else      {          printf("unknow actionn");      }      nowStatus = nextStatus;      printf("next status is %dn",nowStatus);  }  int main(void)  {      nowStatus = SLEEP;      execute(EAT);      return 0;  }

这段代码的意图就非常明显了,首先判断当前所在状态, 然后找到其中的行为,最后执行。 但是这段代码有以下几个特点:

  • 新加一种行为需要修改execute函数
  • 新加一种行为需要增加更多分支代码
  • 新加一种状态,需要新增一个大的分支
  • 哪些状态有哪些行为不是很明显

换一种写法

在《高级指针话题-函数指针》中介绍了函数指针,以及在《编程技巧-跳转表》介绍了函数跳转表。这里我们把代码调整一下,看看结合跳转表和状态机,能写出什么样的代码。

//来源:公众号【编程珠玑】地址:https://www.yanbinghu.com  #include <stdio.h>  /*省略枚举定义和函数定义,与前面相同*/  typedef int (*Handler)(int);//函数指针  /*用于处理某种状态的行为*/  typedef struct Act_Handler  {      int action;      Handler handler;  }Act_Handler;  /*某种状态的处理集*/  typedef struct Stat_Handler  {      int status;      int actNum;      Act_Handler *actHandler;  }Stat_Handler;  /*sleep状态行为处理*/  Act_Handler sleepHandler[] =  {      {SMILE,smile},      {PEE,pee}  };  /*wake状态行为处理*/  Act_Handler weakHandler[] =  {      {SMILE,smile},      {PEE,pee},      {EAT,eat}  };  /*状态处理*/  static Stat_Handler statHandler[] =  {      {SLEEP,sizeof(sleepHandler)/sizeof(Act_Handler),sleepHandler},      {WAKE,sizeof(weakHandler)/sizeof(Act_Handler),weakHandler},  };  static int statSize = sizeof(statHandler)/ sizeof(Stat_Handler);  void execute(int action)  {      if(nowStatus >= statSize)      {          printf("unknow statusn");          return;      }      printf("now status is %d,action %dn",nowStatus,action);      int nextStatus = nowStatus;      Act_Handler *actHandler = statHandler[nowStatus].actHandler;      int actNum = statHandler[nowStatus].actNum;      int actIdx = 0;      /*遍历指定状态下的行为集,找到对应的行为*/      for(actIdx = 0;actIdx < actNum;actIdx++)      {          if(actHandler[actIdx].action == action)          {              nextStatus = (actHandler[actIdx].handler)(action);              break;          }      }      if(actIdx == actNum)      {          printf("did find action %d in status %dn",action,nowStatus);      }      nowStatus = nextStatus;      printf("next status is %dn",nowStatus);  }  int main(void)  {      nowStatus = WAKE;      execute(EAT);      return 0;  }

这里简单说明一下execute函数中的执行流程。

  • 判断当前状态合法性
  • 在数组中找到对应状态的行为处理集
  • 在处理集中找到对应的行为
  • 处理结束

这种方式有什么特点呢?可以看到,在需要新加一个动作的时候,只需要在sleepHandler或者weakHandler中添加,完全不影响execute函数的改动。你甚至可以将不同状态分在不同的文件中管理,使得结构更加清晰明朗。

另外一方面,某种状态下,能执行哪些动作,非常清晰。

不过这样的写法对于初学者来说不太友好,但是不影响你添加新的内容。

有的读者可能会堪虑,在寻找行为的时候,for循环会不会很慢?首先这和case差别不大,通常不会有性能问题,其次除了使用数组,还可以考虑其他数据结构,例如哈希,我在《工作中用不到算法,为什么还要学算法?》中也提到了,数据结构和算法能更好地帮我们解决问题。

总结

本文代码较多,建议在实际环境中运行调试。完整可运行代码也可以阅读原文。