bool変数名でバグを起こさない方法について考える


Bool(ean) 変数の意味の取り違え

ほとんどの人が一度はこの類のバグをやらかしたことがあるだろう:

bool isAvailable = true; // 在庫があるか

if (isAvailable) {
    System.Console.WriteLine("在庫なし"); // Uh oh
} else {
    System.Console.WriteLine("在庫あり");
}

// Prints... 「在庫なし」...???

ifの分岐先の取り違え である。

これは単純なラベルの取り違えだが、ifの内容が多くなってきた場合や、elseで何もしなくていいので省略した場合などには私は割とやらかすことが多かった。

これは私のプロジェクト内にあるコードでのbool変数の命名規則に穴があったことに原因があるのではないかと思い、 boolean変数名でバグを仕込まない工夫 について、命名規則の面から少し考えてみる。

もちろんこの規則が絶対的にバグを防ぐとは決して思ってないし、むしろ読者にとっては逆にバグを仕込みやすいパターンを解説しているかもしれない。その場合はそれに合わせて読み替えていただければ良い。

boolean変数の名前

boolean型の変数は true / false の2値だけを取る型の変数なので、大多数のケースにおいて、Yes/No で答えられる状態について保存するために使うだろう。

それを考えると、boolean変数は大概、このようなネーミングになると考えられる。

  • is....
  • has...
  • can...

私がコードを書くときboolean変数を使う場合には、それが指し示す状態を表す単語だけではなくて英語のbe動詞・助動詞を必ず前につける規約としている。

問題はこの「指し示す状態を表す単語そのもの」だ。

状態を表す単語

こう言った単語には、3パターン考えられる。そのうち2つは、 肯定的単語否定的単語 だ。たとえば:

  • 肯定的単語
    • Available (何かが在ること)
    • Succeeded (成功したこと)
  • 否定的単語
    • Unavailable (何かが無いこと)
    • Failed (失敗したこと)

私はこれらのカテゴリの取り扱いを間違うことは、バグを仕込む原因の一つになると考えている。

肯定的・否定的単語の取り違え

プロジェクト内の複数のboolean変数があって、それぞれが表現している状態が違う状態なら、これらのカテゴリが混在していても直ちには問題にはなりづらいと思われる (ロジックや可読性のためにやることもあろう)。

using System;

bool hasFetchFailed = false; // 否定的単語
bool isAvailable = false; // 肯定的単語

if (hasFetchFailed) {
  Console.WriteLine("通信エラー");
  return;
}

if (isAvailable) {
  Console.WriteLine("在庫あり");
} else {
  Console.WriteLine("在庫なし");
}

ただし、同じ計算式に両方が巻き込まれると事故の元になり得る。

if (!hasFetchFailed && isAvailable) {
  // ...通信が...失敗したら?しなかったら? 一瞬どっちかわからんし、第一このifをひっくり返そうとすると苦労する
}

ましてや、変数が表現する状態が実質同じなのに、論理が反転するような名前を使い出したら大変なことになる。これは特にタチが悪くて、このパターンは大概クラスやメソッドの実装を跨いで発生する。たとえば、その状態がプロジェクト中で広く関わってくる状態であって、各クラスでその状態の判定をするための変数が肯定・否定両方の側面から語られていたら混乱は避けられない。

class A {
  bool hasSucceded; // こっちは肯定的単語なのに
}

class B {
  bool hasFailed; // こっちは否定
}

肯定的単語と否定的単語のどちらを使ったかは記憶しておくべき、というより、 よく扱われる状態については「絶対肯定(否定)的単語」を使う と決めておいて、それから外れる単語はプロジェクトごとに決めて外さないようにするのが良いだろう。例を以下に示すが、当然逆でも構わないし、なんなら全てどちらかに寄せてしまうのも手だ。

例)

状態 カテゴリ 使う単語 理由
通信結果 否定 failed 失敗処理だけ先にしてreturnするパターンはよくあるので
在庫 肯定 available UIのコードで混乱を防ぎたいから

肯定でも否定でもない別のパターン

先ほどbe動詞・助動詞の後の単語は3パターンあると考える といった。もう一つはもちろんそのどちらにも当てはまらないパターンだ。

自分のプロダクトで恐縮だが、今回この投稿をするきっかけとなったプロジェクトなのでそれを例に取る。

私は音楽ゲーム Twin Horizons を制作し公開したのだが、この音楽ゲームには2つのモード(レーンスタイル)が存在する。上下4レーンずつ合計8レーン全部を使う「Full Lanes」モードと、下の4レーンだけ使う「Half Lanes」モードだ。

このゲーム、ロジックの一部に「プレイヤーが遊んでいるのはどっちのレーンスタイルか」が重要になる部分がある。そのロジックを最近見返していたところ、こんな実装をしていることに気づいた (コードは変数名以外実際のものではない)。

// プレイヤーの選択から得た情報としてこの変数(or引数)が存在する
bool isHalf = ???; // 意味としては 「プレイヤーがHalf Lanesで遊んでいるか」のつもり

// その後でそれを使って判定をかける
int rewardCoin;
if (isHalf) {
  rewardCoin = 700;
} else {
  rewardCoin = 800;
}

このケースにおいては、「be動詞・助動詞の後の単語」が肯定・否定どちらでもない

そしてこれが非常にまずいことに気づいた。

Halfの反対、つまり isHalf == false が何を表しているのか見た瞬間にわからない。たとえばこんな書き方をした場合、すぐに Full Lanes の時だけの話をしていることに気づかない、ということだ。

if (!isHalf) {
  // Full Lanesの時だけ実行する処理について書きたかったが、ぱっと見でそれがわかるだろうか?
}

さらにもっとタチの悪いことに、この変数、別の画面でプレイヤーが下した選択として、当該ロジックのある画面のコードに「パラメータ」として渡されているのである。つまり、パラメータ定義の時点で isHalf isFull のどちらの名前なのかは固定されてしまっている。
この画面で「Half Lanes」「Full Lanes」それぞれだけで行う処理がコード中の離れた場所に共存した場合、もう手の施し用がない。

この場合、そもそもbooleanを使うこと自体が間違いだった。 「プレイヤーが遊んだのはHalf Lanesか」という状態で管理することが問題なのだ。当該ゲームではこれ以外のレーンスタイルは余程のことがない限り追加しないと考えていたため、状態が2つだったことも災いした。

完全にこれは 列挙型 enum を使うケースだった。

enum LaneStyle {
  Half,
  Full,
}

LaneStyle style;

こうすれば、ifの判定で比較対象を書かざるを得なくなり、可読性が確保される:

if (style == LaneStyle.Half) {
  // 見ただけで 100% Half Lanesの処理とわかる
}

まとめ

変数名はなんでも良い という自由度ゆえ、バグりやすい選択をしてしまう可能性がある。あえて命名規則を課すことで、それを回避することが可能となるだろう。

  • そもそも、その変数で表す状態を否定した状態は、前提知識なしにわかるか?
    • わかる場合 (e.g., Available なら 否定すれば 在庫がない など)
      • 同じ事象を示す変数はできるだけ肯定側 or 否定側のどちらかに寄せる
    • わからない場合
      • そもそもbooleanをやめてenumにする

このようなルールだけでも、読みやすいコードの第一歩となると考えられる。

余談

  • is...i と、否定に使う ! がフォントによって似ている問題
  • というか ! が括弧まみれの中にあると見落とす問題
if (!isPlayable) {
  // 等幅フォントじゃない時この 「!」 を見落として混乱する
  // (!(something && !anotherThing)) なんてされた日にはもう
}
  • クラスインスタンスのプロパティがbooleanの時に否定かけようとすると ! が物理的に遠くなっていく問題
if (!instance.isActive) {
  // isActive だけ見て読んだらとんでもないことに...
}