Top Of Page 直線的なコード 条件分岐 ループ GoTo Return Bottom

制御構造について

これまで、「名前」と「変数」にこだわってまとめてきました。
ここですこし気分を切り替えましょう。
今回掘り下げてみるのは、現代的なプログラミング言語のほぼすべてがサポートする、制御構文とそれがもたらす構造に関してです。
if とか for とかありますよね?
プログラム言語によっては、大文字で始まるとか、多少書き方が違うかもしれません。しかしこの条件分岐と反復指定こそが、現代的なプログラミング言語の一つの特徴でもあります。
あまりに自然な文脈なので、つい何気なく使ってしまうこれらの構文ですが、より効果的な使い方を目指して、ここで分析してみます。

直線的なコード

さて、制御構造の基本はなんでしょうか?
if ? for ?
いいえ、そうではなく、直線的な、なにも分岐も反復もしないコードが、すべての基本でしょう。
この基本中の基本の構造でも、じつは2種類あります。それは、順序が決まっているコードか、あるいはどの順序でもいいコードか、です。

いうまでもなく、気をつけなければ行けないのは、順序が決まっているコードで、これは順序を間違えるとそれだけでバグになってしまいます。そこで、この順序を間違えないようなプログラミングが効果を発揮します。

たとえば、プログラマの多くはデータを使う前に初期化することを経験的に知っています。だから、xxxxClear() というルーチンは、他のルーチンより先に呼ばれなければならないことがわかります。
また、あるルーチンの出力パラメータ(または戻り値)を使っているなら、もちろんそれを使うところはそのルーチンを呼んだ後でしょう。

ところで、だからといって、どの順序でもいいコードについて何も注意しないのは、片手落ちというものです。
どの順序でもいいコードこそ、意味的に上から下に読めるように書きましょう。
そして、スコープやパーシスタンスを短くまとめ、すっきりしたコード構成にするように心がけましょう。

条件分岐

さて、次に触れるのは条件分岐です。
この条件分岐は、もっとも古いプログラミング言語でも、そして最も機械に近いレベルのプログラミング言語でも、必ず存在する概念です。
コンピュータは、この概念があるからこそ、高度な業務が実行できるのです。
これは逆にいえば、条件分岐を間違えると致命的なバグが発生するということでもあります。
まず、古来から伝わる(笑)、IF系から分析しましょう。
IFでバグを起こさないために、私は原則として以下のことを心がけています。
特に、ifとelseの逆転は、ifに関連するもっとも多いバグの元です。
とくにベテランプログラマの方は、その人なりの価値観やルールを持っているので、ここに書いてあることに対しての反論もあるかと思います。
それでいいと思っております。
特に本来の処理をはじめにおくかどうか…このへんは微妙です。
逆の方も多いかもしれません。複雑なほうを先にもってくるというカンジでしょうか。
私が本来の処理をはじめにもってくるのは、そのルーチンの意義を明確にしたいためです。
このへんは、プログラムを通してポリシーに貫かれていれば、どちらにも一理ある部分でしょう。

さて、IF 以外の条件分岐といったら…そう、CASE系です。
Visual Basic では Select Case、C/C++では switch caseとして知られています。
多くの条件分岐を制御するときに非常に便利な制御構造です。
ここで大事になってくるのは、条件を並べる順番です。どうせ並べるなら読みやすく並べましょう。
どんな並びが読みやすいのか。私が提案するのは以下のような順番です。

頻度的に一般だというのは、「そのcaseを通ることが多い」ということです。これは意味的にも一般であることが多いようです。
また、このCASEでの不具合を減らすために。 ということを心がけましょう。
特にデフォルト節は便利で、いろいろな使い道があります。しかし、本来このデフォルト節(VBならCase Elseとなります)は、その上で並べた条件に当て嵌まらないものが通るためのものです。
便利だからといってこのことを忘れてトリッキーなプログラミングを行うと、保守性可読性が低下してしまいます。

ループ

ループ構造は、数々の構造化プログラミング言語の最大の特徴です。全ての構造化言語は、ループ構造を実装することによって、gotoを使用しなくても済み、繰り返し構造を明確にでき、バグを(それまでの言語と比較して)飛躍的に減らすことに成功しています。

多くの構造化言語は、ループの制御方法を幾通りも用意してくれています。それだけループを重視しているということでしょうが、そのループの制御についても、注意を払ってコーディングしないと、何のループなのかわからなくなります。
制御ポイント ここに注意
ループに入る 一カ所からループに入るようにする
ループの直前に初期化コードを置く
本体を処理する {と}、あるいはBeginとEndで囲む
できるだけ空ループを避ける
一つのループで一つの機能を果たすようにする
ループ管理に関するブロック(インデックスの加算など)は先頭か末尾にまとめる
ループを終了する 終了することをトレースして確認する
終了条件を明確にする
breakとcontinue 一つのループにたくさんbreakを置かないようにする
しかし、ループ脱出のフラグを多く使用するよりはbreakの方がいい
たまに、ループの途中にラベルを置き、そこに向かってgotoで飛び込むような「カッコイイ」コードを書くプログラマがいます。
しかし、そういったケレンな技は、後でそのループを読み難くすることが多く、そのリスクを背負ってまで曲芸コードを書く必要は認められません。
また、一つのループであまりに沢山の機能を実現しようとすると、どうしても長大でネストが深いループになります。ですから、パフォーマンスの制約などが緩いのならば、一つのループに一つの機能というのが判りやすいと言えます。

ループは非常に便利な構造なので、うっかりするといつのまにか長い、大きなループになっていることがあります。
長いループは、非常に注意深く組まないと、そのループが本来なにをするべきループなのかを見失ってしまうことがあります。ご注意を。
できるだけ、下のようなループにしましょう。

また、ネストするループはあくまでも経験的にですが、内側から外側に組むと、組みやすいようです。

GoTo

上の3つが、いわゆる「主だった制御構造」です。
これから紹介するのは構造的には多かれ少なかれ異端的なものを包含していますが、このgotoは現代プログラミングにおいて異端中の異端といえるでしょう。

しかしここで認識しなければならないのは、「よいプログラミングとは、「gotoを使用しない」ことではない」ということです。
よいプログラミングとは、整然とした問題分析、名前付けなども含めたコードの練り直し、適切な制御構造の選択によって構築されるのです。その結果として、gotoは(ほとんど)使われなくなるのです。
gotoをなくすのは目的ではなく、あくまでも結果です。gotoのないプログラムを作ることを目的とするのは、すこし問題を履き違えていると思います。

gotoを使用したほうが、コードが格段に単純になるなら、gotoを使用する価値はあります。
事実、ベテランの腕利きプログラマの多くは、「ここぞ」というところにgotoを使い、gotoなしでは深くなってしまうネストを浅くおさえ、コードを簡潔にし、可読性を向上しています。
しかしやはり無差別に使ってしまうと、gotoはスパゲッティや蕎麦やラーメンやうどん…あるいはそれらが混ざったものを生み出す温床であることも事実です。
ガイドラインを設けて、効率的に使用しましょう。

あまり使い過ぎるのは絶対に避けるべきです。よほどのことが無い限り、ラベルは1つに押えましょう。

Return

return(BasicではExit Functionなども)はルーチンからの任意の脱出手段です。
これを使用すれば、いつでも、そしてルーチンのどこからでも自由に脱出することができます。
しかし、returnを多用すると、確保したメモリを解放し忘れるとか、オープンしたファイルを閉じ忘れるとか、ルーチンのクリーンアップを忘れたりするなどの副作用もありますので注意が必要です。
returnの数はなるべく少なく心がけましょう。
しかしそれではネストが深く読み難くなり、本来そのルーチンで行うべきことがぼやけてしまう場合、そのときはあえてreturnを途中で使用し、全体的な読みやすさを追求するべきです。

再帰

さて、制御構造のトリを飾るのは、再帰構造です。
再帰構造とは、あるルーチンから、そのルーチン自身を呼び出して処理するような構造です。
使う機会さえあれば、使ってみたい…なんとなくカッコよさそうだし、上級者っぽいし…
たしかに、再帰も魅力的な制御構造の一つです。多くの言語は再帰をサポートしていますし、使うべき所に使えば、コードの複雑さを低減する強力な武器となります。
しかし、むやみやたらに再帰を使用すると、非常に読みにくいコードとなります。
そういう意味では、この構造を、そのリスクを背負って、そしてそのリスク以上に効果的に使いこなせるのであれば、確かに上級者と言えるでしょう。
再帰構造を使うチャンスが来ても、以下のことを冷静に検討してください。
再帰でしか表現できないということはありません。必ず他の方法もあります。ですから、その方法と比較し、そしてどちらがより効果的か検討してみてください。
Btree-ISAMのガベージ管理(意味不明かもしれませんが、まあなんか面倒くさそうなことかな、とか思っていただければ十分です(^^;)などで、再帰を複数のルーチンに広げる必要があることもありますが、それは本当にまれです。これを容認してしまうと、よほど慎重に組まないと再帰の迷宮に陥ってしまいます。
もちろん、そのコードを読むときにも、慎重に、そして集中して読まなければなりません。非常に疲れます。

そして、よくプログラミングの入門書で書いてあることで、実際にはやってはいけないことがあります。
階乗に再帰は使わないで!
よく例題などでみます。階乗。
しかし、こんなスタック領域の無駄はありません。普通のループで実装したほうが100倍はマシでしょう(^^;

(11/13/2011 追記)
掲示板に「Sampo」さんからご指摘をいただきました。
「階乗に再帰を使うのはスタックの無駄だとのこと。しかしこの記述は正しくありません。末尾再帰ならスタック使いませんので。
階乗を再帰で書くなら「末尾再帰になるように」書いて、とするべきかと思います。 」
これはご指摘の通りで、末尾再帰の記法と再帰の末尾最適化をしてくれるコンパイラとのセットであれば、末尾再帰はコンパイル時に自動的に反復処理に書き換えられるので、スタックの増加はありません。
階乗に限らず、再帰構造を使う場合は、末尾再帰として記せないかをご検討ください。
末尾再帰に関しての情報は、Web検索をしてみてください。
かなり専門的な用語なので、検索結果にノイズが少なく、効果的に興味深い情報にアクセスできると思います。


はじめに ルーチン モジュール 変数名の力 変数使用法 制御構造 レイアウト コメント テスト デバッグ 謝辞 Top of Site

Copyright (c) 2000 Takao Tamura