あやしいif文

ユキ先輩。プログラムには流れがあるっていう話を聞いて、if文の使い方が大事なんだなと感じたんですけど……
うん。if文は基本だけれど、意外に奥が深いものよ。
ですよね!そしたら、やっぱりコツみたいなものって、いろいろあるんじゃないですか?
そうね……C言語の解説書なんかでは、「やりがちな間違い」がよく紹介されてるわね。
「やりがちな間違い」ですか?
例えばね……

次のif文では、変数xの値が「-10」と「10」の間かどうかチェックしようとしています。

  int x = 123;
  if (-10 < x < 10) {
    puts("Yes");
  } else {
    puts("No");
  }

もし、これが正しいプログラムなら、Noと表示されるはずですね。でも、実際にはYesと表示されるでしょう。理由は、次のとおりです。

  • このif文は、-10 < x < 10の計算結果がtruefalseかをチェックする
  • 最初に、-10 < xの部分がtrueと判定される(x123なので)
  • 次に、これをもとの式に当てはめると1 < 10となる(C言語ではtrue1false0なので)
  • その結果、式全体の計算結果はtrueとなる

さらによく考えてみると、この式の計算結果はxがいくつでもtrueになることが分かります。-10 < xの部分は必ず10になり、どちらにしても10より小さいためです。

うわぁ。なんだか、思ってたのと違う計算になっちゃってますね……。
これはね、書き方を間違えているのよ。
じゃあ、正しい書き方があるってことですか?
もちろんよ!

上のif文は、次のように書き直せば期待したとおりに動きます。

  if ((-10 < x) && (x < 10)) {
    puts("Yes");
  } else {
    puts("No");
  }

-10 < x < 10-10 < xx < 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");
xyで割り切れるかどうかを、2つのif文で調べているわね。
ふむふむ。割り算をする前に、分母にくるyの値が0じゃないことを1つ目のifで確認してるんですね。
そう。この2つの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つにまとめてしまえばいいのよ。
もしかして!さっきも出てきた「AND条件(&&)」ですね?
つまり……
  /* xは3と5の倍数? */
  bool isMultiple = false;
  if ((x % 3 == 0) && (x % 5 == 0)) {
    isMultiple = true;
  }
正解よ!if文が1つ減ったわね。
やったー!
ちなみに、もう1つ減らせるのだけど……
いやいや、それだとifがなくなっちゃいますけど。
なくすのよ。
判定結果を変数に格納するだけなら、これでいいわね。
  /* xは3と5の倍数? */
  bool isMultiple = (x % 3 == 0) && (x % 5 == 0);
なんと!そもそもif文が要らない処理だったんですね。
ここがポイント!
「AND条件(&&)」をうまく使うと、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でも2でもないケースについて、考えるのを忘れてしまったことが原因よ。

関数の定義域を明確にして、次のようにすれば状況は改善されます。

/* 引数の値に応じたあいさつを表示する。
 *   1のとき: "Hello!"と表示
 *   2のとき: "Bye!"と表示
 * 上記以外の値は入力禁止とする(入力時の動作は未定義)。
 */
void Say(int greetingIdentifier) {
  assert((greetingIdentifier == 1) || (greetingIdentifier == 2));

  if (greetingIdentifier == 1) {
    puts("Hello!");
  }
  else {
    puts("Bye!");
  }
}

コメントに1文追加したのが分かるでしょうか。これで、3を引数としてこの関数を呼び出してはいけないことが明確になりました。

実装としては、assertが追加されただけですね。
そうね。でも、この1行が書けるようになるとプログラムの品質はぐんと向上するのよ。

こうすれば、たとえ呼び出し側が誤って引数に3を指定したとしても、それが間違いだとすぐに気付くことができるでしょう。関数の誤った呼び出し方を、その入り口でシャットアウトしているからです。

if文のほうは何も変わっていませんね。
そうね。でも、すぐ上にassertがあるのがポイントよ。引数の値が1でも2でもないケースについて、しっかり考慮されていることが伝わるでしょう?
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を追加しやすいってことですよね。
正解!引数で3や4を受け付けられるように機能を拡張したくなったときも、これなら対応しやすいわね。

この書き方なら、将来の機能拡張が既存部分に与える影響を最小限にできるでしょう。引数の値のバリエーションが実際に「増えそうだな」と予想されるときは、こうしておければベターだといえます。