這個編程技巧別說我沒告訴你

  • 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差別不大,通常不會有性能問題,其次除了使用數組,還可以考慮其他數據結構,例如哈希,我在《工作中用不到演算法,為什麼還要學演算法?》中也提到了,數據結構和演算法能更好地幫我們解決問題。

總結

本文程式碼較多,建議在實際環境中運行調試。完整可運行程式碼也可以閱讀原文。