CODE COMPLETE

~特殊な制御構造~

エンジン開発6 鈴木優

アジェンダ

  • 複数のreturn文
  • 再帰
  • goto文
  • 特殊な制御構造の展望
  • まとめ

return文

関数から複数のreturn

答えがわかった時点でreturnする

int Compare(int value1, int value2) {
    if (value1 < value2) {
        return -1;
    } else if (value1 > value2) {
        return 1;
    }
    return 0;
}

関数から複数のreturn

複数のエラー条件を確認する例

//ファイル名チェック
if (file.validName()) {
    //ファイルが開けたかチェック
    if (file.open()) {
        //暗号キーが入力されているかチェック
        if (encryptionKey.valid()) {
            //暗号キーが正しいかチェック
            if (file.decrypt(encryptionKey)) {
                //正常時の処理
            }
        }
    }
}

インデントが深すぎて読むのが苦痛・・・

関数から複数のreturn

ガード句を使って単純化しよう!!

if (!file.validName()) {
    return;
}
if (!file.open()) {
    return;
}
if (!encryptionKey.valid()) {
    return;
}
if (!file.decrypt(encryptionKey)) {
    return;
}

//正常時の処理が以下に続く

答えがわかった時点で処理を戻す

関数から複数のreturn

さらによくするなら、エラー内容をしっかり上位層に伝えてあげる

if (!file.validName()) {
    return FILE_INVALID_NAME_ERROR;
}
if (!file.open()) {
    return FILE_OPEN_ERROR;
}
if (!encryptionKey.valid()) {
    return INVALID_ENCRYPTION_KEY;
}
if (!file.decrypt(encryptionKey)) {
    return DECRYPTION_ERROR;
}

//正常時の処理が以下に続く
    

複数returnの注意点

各関数のreturn文は最小限に抑える

  • 関数の最後の部分だけに目を通した場合、
    その手前のどこかで処理が戻されている可能性に
    気づかないと、関数を理解するのが困難になってしまうため

所感など

早めに返せるものは返してしまってよいのでは?

そもそも関数の最後のreturnだけをみるパターンって
そんなにある?

再帰

再帰の例

  • 進める方向は上下左右
  • 再帰を利用して迷路を抜ける
  • ゴールが見つかるまですべての経路を試しながら進む

--------

|

|

|

|

|

迷路を解く

  • 無限再帰を防ぐために、
    すでに通った道は記憶しておく
     
  • 元の制御に
    trueが返ってきたら出口あり
    falseが返ってきたら出口なし
bool FindPathThroughMaze(Maze maze, Point position) {
    //既に通った道は試さない
    if (AlreadyTried(maze, position)) {
        return false;
    }

    //この地点が出口であれば、成功を返す
    if (ThisIsTheExit(maze, position)) {
        return true;
    }

    //この地点をすでに通ったものとして記憶する
    RememberPosition(maze, position);

    //上下左右への経路を確認する
    //いずれかの経路が成功すれば、終了する
    if (MoveLeft(maze, position, &newPosition)) {
        if (FindPathThroughMaze(maze, newPosition)) {
            return true;
        }
    }
    if (MoveUp(maze, position, &newPosition)) {
        if (FindPathThroghMaze(maze, newPosition)) {
            return true;
        }
    }
    if (MoveDown(maze, position, &newPosition)) {
        if (FindPathThroghMaze(maze, newPosition)) {
            return true;
        }
    }
    if (MoveRight(maze, position, &newPosition)) {
        if (FindPathThroughMaze(maze, newPosition)) {
            return true;
        }
    }
    return false;
}

再帰の利用に関するヒント

  • 再帰を抜けるパスが含まれていることを確認する
  • 安全カウンタを使って無限再帰を防ぐ
  • 再帰をひとつの関数に限定する
void RecursiveMethod(int* safetyCounter) {
    if (*safetyCounter > SAFETY_LIMIT) {
        return;
    }

    (*safetyCounter)++;
    //何らかの処理
    recursiveMethod(safetyCounter);
}

まとめ

  • 再帰を使う場合は、必ず他の手法を検討する
     
  • 複数の関数にまたがる再帰はしない
     
  • スタックオーバーフローに気をつける

所感など

再帰は頭が痛くなるので使いたくないし読みたくない!!

goto文

goto反対論

  • 一般的には、goto文のないコードのほうが品質が良いとされる
    • フォーマットが難しくなる
    • コンパイラの最適化が無効になる
    • コードが上から下に流れるという原則に反する
    • javaをはじめとする現代の多くの言語では存在しない

goto賛成論

  • gotoを見境なく利用するのではなく、特定の条件化で慎重に利用することを支持する意見が多い
  • gotoを上手く配置できれば、コードの重複がなくなる
  • gotoはリソースを割り当て、リソースを処理し、
    リソースを解放する関数に役立つ
  • 場合によってはgotoを利用するとコードが速くなり、
    サイズが小さくなることがある

エラー処理とgoto文

エラーが発生したらリソースを解放する(goto使用例)

//複数のファイルを削除する
void DeleteFiles(ErrorCode* error_code) {
    *error_code = ErrorCode.FILE_STATUS_SUCCESS;
    std::list<File> file_list;

    //削除するファイルを取得
    MakeDeleteFileList(&file_list);

    std::iterator it = file_list.begin();
    while (it != file_list.end()) {
        //ファイルオープンチェック
        if (!OpenFile(*it)) {
            *error_code = ErrorCode.FILE_STATUS_OPEN_ERROR;
            goto FIN;
        }
        //ファイル上書きチェック
        if (!OverwriteFile(*it)) {
            *error_code = ErrorCode.FILE_STATUS_OVERWRITE_ERROR;
            goto FIN;
        }
        //ファイル削除チェック
        if (!DeleteFile(*it)) {
            *error_code = ErrorCode.FILE_STATUS_DELETE_ERROR;
            goto FIN;
        }
        ++it;
    }

FIN:
    RemoveFileList(&file_list);
}

エラー処理とgoto文

gotoを避ける為にネストする例

//複数ファイルを削除する
void DeleteFiles(ErrorCode* error_code) {
    error_code = ErrorCode.FILE_STATUS_SUCCESS;
    std::list<File> file_list;

    //削除するファイルを取得
    MakeDeleteFileList(&file_list);

    std::iterator it = file_list.begin();
    while (it != file_list.end() && *error_code == ErrorCode.FILE_STATUS_SUCCESS) {
        if (OpenFile(*it)) {
            if (OverwriteFile(*it)) {
                if (!DeleteFile(*it)) {
                    *error_code = ErrorCode.FILE_STATUS_DELETE_ERROR;
                }
            } else {
                *error_code = ErrorCode.FILE_STATUS_OVERWRITE_ERROR;
            }
        } else {
            *error_code = ErrorCode.FILE_STATUS_OPEN_ERROR;
        }
        ++it;
    }
    RemoveFileList(&file_list);
}

エラー処理とgoto文

gotoを避ける為に状態変数を利用する例

//複数ファイルを削除する
void DeleteFiles(ErrorCode* error_code) {
    error_code = ErrorCode.FILE_STATUS_SUCCESS;
    std::list<File> file_list;

    //削除するファイルを取得
    MakeDeleteFileList(&file_list);

    std::iterator it = file_list.begin();
    while (it != file_list.end() && *error_code == ErrorCode.FILE_STATUS_SUCCESS) {
        if (!OpenFile(*it)) {
            *error_code = ErrorCode.FILE_STATUS_OPEN_ERROR;
        }
        if (*error_code == ErrorCode.FILE_STATUS_SUCCESS) {
            if (!OverwriteFile(*it)) {
                *error_code = ErrorCode.FILE_STATUS_OVERWRITE_ERROR;
            }
        }
        if (*error_code == ErrorCode.FILE_STATUS_SUCCESS) {
            if (!DeleteFile(*it)) {
                *error_code = ErrorCode.FILE_STATUS_DELETE_ERROR;
            }
        }
        ++it;
    }
    RemoveFileList(&file_list);
}

エラー処理とgoto文

try-finallyで書き直す例

//複数ファイルを削除する
void DeleteFiles(ErrorCode* error_code) {
    error_code = ErrorCode.FILE_STATUS_SUCCESS;
    std::list<File> file_list;

    //削除するファイルを取得
    MakeDeleteFileList(&file_list);

    std::iterator it = file_list.begin();
    try {
        while (it != file_list.end()) {
            OpenFile(*it);
            OverwriteFile(*it);
            DeleteFile(*it);
            ++it;
        }
    } finally {
        RemoveFileList(&file_list);   
    }
}

4つの手法の比較

どれも一長一短・・・

  • gotoを利用する方法は深いネストと無駄な評価をなくすが、
    もちろんgoto文が入ってしまう (goto警察に絡まれる)
  • ネストしたif文はgotoをなくすものの、深いネストがロジックを複雑に見せる
  • 状態変数を利用する方法はgotoと深いネストをなくすが、その文評価の数が増える
  • try-finallyはgotoと深いネスト両方をなくすが、
    すべての言語で利用できるわけではない

4つの手法の比較

  • try-finallyが使えるなら使うのが最も単純
  • それ以外の場合は、goto文やネストしたif文よりも状態変数を利用した方法がよい
    • コードが読みやすくなる
    • ロジックがうまくモデリングされる
       
  • どの方法を選択するにせよ、プロジェクトのすべてのコードに一貫性を持たせることが大事

まとめ

  • goto文の柔軟性につけこまないこと
  • goto文を利用する場合は前方に進むものだけにする事
  • goto文によってアクセスできないコードがない事
  • goto文を他の方法で書き換える方法はいくつかあるので、
    問題への妥当な解決策がgotoしかない場合は
    その旨を明記した上で利用すること
  • 他のプログラマが提案するgoto文を利用しない方法に
    心を開くこと

所感

使うかどうか悩むことが今後あるかすら怪しい

新しく書くコードにgotoは使わない

特殊な制御構造の展望

特殊な制御構造の展望

かつてgoto文は良い考えとみなされていた

  • goto文を無制限に利用すること
  • goto文のターゲットを動的に計算し、その場所にジャンプすること
  • goto文を使って関数の途中から別の関数の途中にジャンプすること
  • 行番号やラベルを使って関数を呼び出し、関数の途中で
    実行を開始すること

特殊な制御構造の展望

ソフトウェア開発はプログラマがコードを使ってできることを
制限することによって大きく前進してきた
 

今後、ここで登場した制御構造は
ゴミ箱に捨てられる運命にあるのではないか

まとめ

まとめ

  • 複数のreturn文は関数の可読性と保守性を向上させ、
    深くネストしたロジックを避けることに役立つ
     
  • 再帰は、問題の範囲が狭い場合の的確な解決策になりうる但し、慎重に使うこと
     
  • 可読性と保守性の良いコードを書くために、goto文が最善の方法になるケースがいくつかある
    しかし、そのようなケースはまれである
    gotoはあくまで最後の手段として利用すること

資料作りが下手でもそれっぽく見せれる気がする

Copy of CODE COMPLETE第6回レビュー

By Samil Vargas

Copy of CODE COMPLETE第6回レビュー

特殊な制御構造

  • 708