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

前言

有讀者在後臺留言說用c寫一篇有限狀態機的推文,正好之前也用過,就分享一下吧。

背景

先舉一個簡單的例子,假設是這樣的,一個小孩有兩種狀態,睡眠,清醒。睡的時候可能會撒尿,微笑,撒尿之後會轉爲清醒狀態,而清醒的時候可能會笑,會喫,喫完之後會轉會睡眠狀態
用C語言實現,一般寫法可能是這樣的:

//來源:公衆號【編程珠璣】
#include <stdio.h>
enum KID_STATUS
{
    SLEEP = 0,
    WEAK = 1,
    INVALID = 2
};
enum KID_ACTION
{
    SMILE = 0,
    EAT = 1,
    PEE = 2
};
/*孩子當前狀態*/
static int nowStatus = WEAK;

/*孩子行爲*/
int smile(int status)
{
    printf("kid smile\n");
    return status;
}
int eat(int status)
{
    printf("kid eat\n");
    return SLEEP;
}
int pee(int status)
{
    printf("kid pee\n");
    return WEAK;
}
/*孩子產生某個行爲*/
void execute(int action)
{
    int nextStatus;
    /*醒着時的行爲*/
    if(WEAK == 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 weak %d\n",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 %d\n",action);
                nextStatus = nowStatus;
                break;
        }
    }
    else
    {
        printf("unknow action\n");
    }
    nowStatus = nextStatus;
    printf("next status is %d\n",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}
};
/*weak狀態行爲處理*/
Act_Handler weakHandler[] = 
{
    {SMILE,smile},
    {PEE,pee},
    {EAT,eat}
};
/*狀態處理*/
static Stat_Handler statHandler[] = 
{
    {SLEEP,sizeof(sleepHandler)/sizeof(Act_Handler),sleepHandler},
    {WEAK,sizeof(weakHandler)/sizeof(Act_Handler),weakHandler},
};
static int statSize = sizeof(statHandler)/ sizeof(Stat_Handler);
void execute(int action)
{
    if(nowStatus >= statSize)
    {
        printf("unknow status\n");
        return;
    }
    printf("now status is %d,action %d\n",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 %d\n",action,nowStatus);
    }
    nowStatus = nextStatus;
    printf("next status is %d\n",nowStatus);
}
int main(void)
{
    nowStatus = WEAK;
    execute(EAT);
    return 0;
}

這裏簡單說明一下execute函數中的執行流程。

  • 判斷當前狀態合法性

  • 在數組中找到對應狀態的行爲處理集

  • 在處理集中找到對應的行爲

  • 處理結束

這種方式有什麼特點呢?可以看到,在需要新加一個動作的時候,只需要在sleepHandler或者weakHandler中添加,完全不影響execute函數的改動。你甚至可以將不同狀態分在不同的文件中管理,使得結構更加清晰明朗。

另外一方面,某種狀態下,能執行哪些動作,非常清晰。

不過這樣的寫法對於初學者來說不太友好,但是不影響你添加新的內容。

有的讀者可能會堪慮,在尋找行爲的時候,for循環會不會很慢?首先這和case差別不大,通常不會有性能問題,其次除了使用數組,還可以考慮其他數據結構,例如哈希,我在《工作中用不到算法,爲什麼還要學算法?》中也提到了,數據結構和算法能更好地幫我們解決問題。

首發:公衆號【編程珠璣】

作者:守望先生

ID:shouwangxiansheng

總結

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

推薦閱讀:

工作中幾乎用不到算法,爲何要學算法?

爲何優先選用unique_ptr而不是裸指針?

擁抱智能指針,告別內存泄露

爲什麼執行自己的程序要在前面加./

關注公衆號【編程珠璣】,獲取更多Linux/C/C++/算法/計算機基礎/工具等原創技術文章。後臺免費獲取經典電子書和視頻資源

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章