21

この質問は決まり文句に聞こえるかもしれませんが、私はここにいる状況にあります。

Cで特定の文字列を解析するために有限状態オートマトンを実装しようとしています。コードを書き始めたとき、ラベルを使用してさまざまな状態をマークし、gotoを使用して1つの状態から場合によっては別の。

この場合、標準のブレーク変数とフラグ変数を使用するのは非常に面倒で、状態を追跡するのは困難です。

どのアプローチが優れていますか?インターンシップをしているので、何よりも上司に悪い印象を与えるのではないかと心配しています。

4

9 に答える 9

37

本質的に悪いことは何もありませんgoto。それらがしばしば「タブー」と見なされる理由は、一部のプログラマー(多くの場合アセンブリの世界から来ている)がそれらを使用して、理解するのがほぼ不可能な「スパゲッティ」コードを作成する方法のためです。gotoコードをクリーンで読みやすく、バグのない状態に保ちながらステートメントを使用できる場合は、より強力になります。

ステートごとにステートメントとコードのセクションを使用gotoすることは、ステートマシンを作成する1つの方法です。もう1つの方法は、現在の状態を保持する変数を作成し、switchステートメント(または同様のもの)を使用して、状態変数の値に基づいて実行するコードブロックを選択することです。この2番目の方法を使用した適切なテンプレートについては、AidanCullyの回答を参照してください。

実際には、2つの方法は非常に似ています。状態変数メソッドを使用してステートマシンを記述し、それをコンパイルする場合、生成されるアセンブリは、gotoメソッドを使用して記述されたコードに非常によく似ている可能性があります(コンパイラの最適化のレベルによって異なります)。このgoto方法は、状態変数法から余分な変数とループを最適化するものと見なすことができます。どちらの方法を使用するかは個人的な選択の問題であり、実用的で読みやすいコードを作成している限り、上司が一方の方法をもう一方の方法で使用することに違いを感じないことを願っています。

このコードを、すでにステートマシンが含まれている既存のコードベースに追加する場合は、すでに使用されている規則に従うことをお勧めします。

于 2010-06-23T22:03:47.190 に答える
19

ステートマシンを実装するためにを使用するgotoことは、多くの場合、理にかなっています。gotoの使用について本当に心配している場合、合理的な代替手段は、多くの場合state、変更する変数と、それにswitch基づくステートメントを用意することです。

typedef enum {s0,s1,s2,s3,s4,...,sn,sexit} state;

state nextstate;
int done = 0;

nextstate = s0;  /* set up to start with the first state */
while(!done)
   switch(nextstate)
      {
         case s0:
            nextstate = do_state_0();
            break;
         case s1:
            nextstate = do_state_1();
            break;
         case s2:
            nextstate = do_state_2();
            break;
         case s3:
             .
             .
             .
             .
         case sn:
            nextstate = do_state_n();
            break;
         case sexit:
            done = TRUE;
            break;
         default:
            /*  some sort of unknown state */
            break;
      }
于 2010-06-23T21:42:31.627 に答える
15

上司に良い印象を残したいのであれば、RagelのようなFSMジェネレーターを使用します。

このアプローチの主な利点は、ステートマシンをより高いレベルの抽象化で記述でき、gotoとswitchのどちらを使用するかを気にする必要がないことです。Ragelの特定のケースでは、FSMのきれいな図を自動的に取得し、任意の時点でアクションを挿入し、状態の量やその他のさまざまな利点を自動的に最小化できることは言うまでもありません。生成されたFSMも非常に高速であることを述べましたか?

欠点は、デバッグが難しく(自動視覚化はここで大いに役立ちます)、新しいツールを学ぶ必要があることです(単純なマシンを使用していて、マシンを頻繁に作成する可能性が低い場合は、おそらく価値がありません。 )。

于 2010-06-23T21:39:52.237 に答える
10

私はあなたがどのような状態にあるかを追跡する変数とそれらを処理するためのスイッチを使用します:

fsm_ctx_t ctx = ...;
state_t state = INITIAL_STATE;

while (state != DONE)
{
    switch (state)
    {
    case INITIAL_STATE:
    case SOME_STATE:
        state = handle_some_state(ctx)
        break;

    case OTHER_STATE:
        state = handle_other_state(ctx);
        break;
    }
}
于 2010-06-23T21:41:53.443 に答える
8

後藤は必ずしも悪ではなく、私はデニスに強く反対しなければなりません。そうです、後藤はほとんどの場合悪い考えかもしれませんが、用途はあります。gotoの最大の懸念は、いわゆる「スパゲッティコード」、追跡不可能なコードパスです。それを回避でき、コードがどのように動作するかが常に明確であり、gotoを使用して関数から飛び出さない場合は、gotoに反対するものは何もありません。注意して使用してください。使用したい場合は、実際に状況を評価して、より良い解決策を見つけてください。これができない場合は、gotoを使用できます。

于 2010-06-23T21:43:58.053 に答える
8

(回避gotoするために)追加された複雑さがより混乱しない限り、回避してください。

実際のエンジニアリングの問題では、gotoを非常に控えめに使用する余地があります。学者や非エンジニアは、を使用して不必要に指を絞っていgotoます。とは言うものの、多くが唯一の方法である実装コーナーに自分自身を塗りつぶす場合gotoは、ソリューションを再考してください。

通常、正しく機能するソリューションが主な目的です。(複雑さを最小限に抑えることによって)正しく保守可能することには、ライフサイクルに多くの利点があります。最初に機能させてから、できれば醜さを単純化して削除することにより、徐々にクリーンアップします。

于 2010-06-23T21:47:50.403 に答える
4

特定のコードはわかりませんが、次のような理由があります。

typedef enum {
    STATE1, STATE2, STATE3
} myState_e;

void myFsm(void)
{
    myState_e State = STATE1;

    while(1)
    {
        switch(State)
        {
            case STATE1:
                State = STATE2;
                break;
            case STATE2:
                State = STATE3;
                break;
            case STATE3:
                State = STATE1;
                break;
        }
    }
}

あなたのために働かないだろうか?を使用せずgoto、比較的簡単にフォローできます。

編集:これらのState =フラグメントはすべてDRYに違反しているため、代わりに次のようなことを行う可能性があります。

typedef int (*myStateFn_t)(int OldState);

int myStateFn_Reset(int OldState, void *ObjP);
int myStateFn_Start(int OldState, void *ObjP);
int myStateFn_Process(int OldState, void *ObjP);

myStateFn_t myStateFns[] = {
#define MY_STATE_RESET 0
   myStateFn_Reset,
#define MY_STATE_START 1
   myStateFn_Start,
#define MY_STATE_PROCESS 2
   myStateFn_Process
}

int myStateFn_Reset(int OldState, void *ObjP)
{
    return shouldStart(ObjP) ? MY_STATE_START : MY_STATE_RESET;
}

int myStateFn_Start(int OldState, void *ObjP)
{
    resetState(ObjP);
    return MY_STATE_PROCESS;
}

int myStateFn_Process(int OldState, void *ObjP)
{
    return (process(ObjP) == DONE) ? MY_STATE_RESET : MY_STATE_PROCESS;
}

int stateValid(int StateFnSize, int State)
{
    return (State >= 0 && State < StateFnSize);
}

int stateFnRunOne(myStateFn_t StateFns, int StateFnSize, int State, void *ObjP)
{
    return StateFns[OldState])(State, ObjP);
}

void stateFnRun(myStateFn_t StateFns, int StateFnSize, int CurState, void *ObjP)
{
    int NextState;

    while(stateValid(CurState))
    {
        NextState = stateFnRunOne(StateFns, StateFnSize, CurState, ObjP);
        if(! stateValid(NextState))
            LOG_THIS(CurState, NextState);
        CurState = NextState;
    }
}

もちろん、これは最初の試みよりもはるかに長いです(DRYの面白いこと)。State =ただし、より堅牢でもあります。状態関数の1つから状態を返さないと、以前のコードの欠落を黙って無視するのではなく、コンパイラの警告が発生します。

于 2010-06-23T21:42:22.547 に答える
1

gotoとswitchの違いはあまりわかりません。私はswitch/whileを好むかもしれません。なぜなら、それはあなたに切り替え後に実行することが保証された場所を与えるからです(あなたがロギングとあなたのプログラムについての理由を投げることができる場所)。GOTOを使用すると、ラベルからラベルへとジャンプし続けるだけなので、ログをスローするには、すべてのラベルに配置する必要があります。

しかし、それを除けば、大きな違いはないはずです。いずれにせよ、それを関数に分割せず、すべての状態がすべてのローカル変数を使用/初期化するわけではない場合、どの状態がどの変数を変更したかわからず、デバッグ/理由を非常に困難にするほとんどスパゲッティコードの混乱に終わる可能性があります約。

余談ですが、正規表現を使用して文字列を解析できますか?ほとんどのプログラミング言語には、それらを使用できるライブラリがあります。正規表現は、多くの場合、実装の一部としてFSMを作成します。一般に、正規表現は任意にネストされていないアイテムに対して機能し、その他すべてに対してパーサジェネレーター(ANTLR / YACC / LEX)があります。一般に、基礎となるステートマシンよりも文法/正規表現を維持する方がはるかに簡単です。また、あなたはインターンシップに参加していると言いましたが、一般的には上級開発者よりも簡単に作業できる可能性があるため、正規表現が文字列で機能する可能性が高くなります。また、大学では正規表現は一般的に強調されていないため、Googleを使用してそれらを読んでみてください。

于 2010-06-23T22:40:27.743 に答える
1

私はあなたに「ドラゴンブック」をお勧めします:コンパイラー、原則-テクニック-アホ、セティ、ウルマンのツール。(購入するのはかなり高価ですが、確かに図書館で見つけることができます)。そこでは、文字列を解析して有限のオートマトンを構築するために必要なものがすべて見つかります。で見つけることができる場所はありませんgoto。通常、状態はデータテーブルであり、遷移は次のような関数です。accept_space()

于 2010-06-23T21:43:12.593 に答える