if文の使い方が大事なんだなと感じたんですけど……
if文は基本だけれど、意外に奥が深いものよ。
次のif文では、変数xの値が「-10」と「10」の間かどうかチェックしようとしています。
int x = 123;
if (-10 < x < 10) {
puts("Yes");
} else {
puts("No");
}
もし、これが正しいプログラムなら、Noと表示されるはずですね。でも、実際にはYesと表示されるでしょう。理由は、次のとおりです。
- この
if文は、-10 < x < 10の計算結果がtrueかfalseかをチェックする - 最初に、
-10 < xの部分がtrueと判定される(xは123なので) - 次に、これをもとの式に当てはめると
1 < 10となる(C言語ではtrueは1、falseは0なので) - その結果、式全体の計算結果は
trueとなる
さらによく考えてみると、この式の計算結果はxがいくつでもtrueになることが分かります。-10 < xの部分は必ず1か0になり、どちらにしても10より小さいためです。
上のif文は、次のように書き直せば期待したとおりに動きます。
if ((-10 < x) && (x < 10)) {
puts("Yes");
} else {
puts("No");
}
-10 < x < 10を-10 < xとx < 10の2つの式に分けて、「AND条件(&&)」でつなげればいいんですね。
if文を整理するテクニックを覚えましょう!
if文のネストを減らす
一般的に、分岐が増えるほどプログラムは複雑になっていきます。デバッグのことを考えれば、if文はできる限り少ないほうがいいでしょう。これは、「必要なif文」と「無駄なif文」を見極められるようになれば可能です。
if文って、ネストのところに多いんじゃないかしら。
まずは、if文がネストしている部分に注目してみるのがおすすめです。次のプログラムを見てください。
int x = 15;
int y = 5;
/* xはyで割り切れる? */
bool isFactor = false;
if (y != 0) {
if (x % y == 0) {
isFactor = true;
}
}
/* 結果を表示 */
puts(isFactor ? "Yes" : "No");
xがyで割り切れるかどうかを、2つのif文で調べているわね。
yの値が0じゃないことを1つ目のifで確認してるんですね。
ifには、このとおりの順番で実行する意味があるということよ。
このように、「最初にこちら」「次にこちら」と順番に判定することがあるなら、if文をネストさせるのは自然な流れです。無理をしてif文の数を減らそうとする必要はないでしょう。
では、次のプログラムはどうでしょうか?
int x = 15;
/* xは3と5の倍数? */
bool isMultiple = false;
if (x % 3 == 0) {
if (x % 5 == 0) {
isMultiple = true;
}
}
/* 結果を表示 */
puts(isMultiple ? "Yes" : "No");
xが3と5の両方の倍数かどうかを調べているわね。2つのifの順番には……
if文はね、1つにまとめてしまえばいいのよ。
&&)」ですね?
/* xは3と5の倍数? */
bool isMultiple = false;
if ((x % 3 == 0) && (x % 5 == 0)) {
isMultiple = true;
}
if文が1つ減ったわね。
ifがなくなっちゃいますけど。
/* xは3と5の倍数? */
bool isMultiple = (x % 3 == 0) && (x % 5 == 0);
if文が要らない処理だったんですね。
&&)」をうまく使うと、if文のネストを減らせる場合があります。
if文で引数を判定する
if文と引数との関係について考えてみましょうか。
if文と引数ですか?
引数の値で処理を分岐させたいときにも、if文はよく使われます。次の関数を見てください。
/* 引数の値に応じたあいさつを表示する。
* 1のとき: "Hello!"と表示
* 2のとき: "Bye!"と表示
*/
void Say(int greetingIdentifier) {
if (greetingIdentifier == 1) {
puts("Hello!");
}
else {
puts("Bye!");
}
}
enumを使って次のように書き直してみれば、あやしい部分が浮き彫りになるでしょう。
typedef enum {
Hello = 1,
Bye = 2
} GreetingIdentifier;
void Say(GreetingIdentifier greetingIdentifier) {
if (greetingIdentifier == Hello) {
puts("Hello!");
}
else {
puts("Bye!");
}
}
引数が2の場合について、まともに判定していなかったことが分かるのではないでしょうか。これでは、引数が3の場合にも2と同じあいさつが表示されてしまいます。つまり、想定外の値が入力されたときにも、2が入力されたのと同じように動作してしまうのです。
その結果、引数の値に問題があってもなんとなく動作してしまいます。そのため、バグがあると認識するのが遅れることになるでしょう。
関数の定義域を明確にして、次のようにすれば状況は改善されます。
/* 引数の値に応じたあいさつを表示する。
* 1のとき: "Hello!"と表示
* 2のとき: "Bye!"と表示
* 上記以外の値は入力禁止とする(入力時の動作は未定義)。
*/
void Say(int greetingIdentifier) {
assert((greetingIdentifier == 1) || (greetingIdentifier == 2));
if (greetingIdentifier == 1) {
puts("Hello!");
}
else {
puts("Bye!");
}
}
コメントに1文追加したのが分かるでしょうか。これで、3を引数としてこの関数を呼び出してはいけないことが明確になりました。
こうすれば、たとえ呼び出し側が誤って引数に3を指定したとしても、それが間違いだとすぐに気付くことができるでしょう。関数の誤った呼び出し方を、その入り口でシャットアウトしているからです。
if文のほうは何も変わっていませんね。
assert()については別のトピックで詳しく説明します。
if文でチェックするときは、想定外の値が入力されるケースについても考えましょう。
if文をswitch文に置き換える
ちなみに、さきほどのif文は、次のようにswitch文で置き換えることもできます。
/* 引数の値に応じたあいさつを表示する。
* 1のとき: "Hello!"と表示
* 2のとき: "Bye!"と表示
* 上記以外の値は入力禁止とする(入力時の動作は未定義)。
*/
void Say(int greetingIdentifier) {
switch (greetingIdentifier) {
case 1:
puts("Hello!");
break;
case 2:
puts("Bye!");
break;
default:
assert(false);
}
}
switch文もif文と同様、プログラムを分岐させるためのものです。複数の分岐を並べて書けるので、うまく使えばプログラムの流れを整理できるでしょう。
caseを追加しやすいってことですよね。
この書き方なら、将来の機能拡張が既存部分に与える影響を最小限にできるでしょう。引数の値のバリエーションが実際に「増えそうだな」と予想されるときは、こうしておければベターだといえます。
