Edited at

マジックナンバーの回避の手引き

マジックナンバーを説明した記事って意外に少ないのね……


はじめに

どのプログラミング言語においても、マジックナンバーは (goto文と同じように) 忌み嫌われている存在です。しかし、マジックナンバーを乱用するプログラマーや、意味や対処法を知らずにとんでもないコードを書いてしまうプログラマーは意外に多いようです。

しかも困ったことに、マジックナンバーを扱った記事やブログはいくつか存在するものの、内容や情報が統一されておらず、調べている人にとっては混乱せざるを得ない状態になっています。

そこで、マジックナンバーの定義から見分け方や対処法、そして「なせマジックナンバーがいけないのか」を一つにまとめた記事を書くことにしました。


マジックナンバーとは?

最初に、マジックナンバーの定義と由来を見ていきましょう。

IT用語辞典では「マジックナンバー」を以下のように定義しています。


マジックナンバーとは、コンピュータプログラムのソースコードなどに直に記述された数値で、その意味や意図が記述した本人以外には自明ではないもの。

リンク:マジックナンバーとは - IT用語辞典


また Wikipedia では、マジックナンバーの由来も記述されています。


そのプログラムを書いた時点では製作者は数値の意図を把握しているが、他のプログラマーまたは製作者本人がマジックナンバーの意図を忘れたときに閲覧すると「この数字の意味はわからないが、とにかくプログラムは正しく動く。まるで魔法の数字だ」という皮肉を含む。

リンク:マジックナンバー (プログラム) - Wikipedia


プログラムで数値を扱う際には、その数値が何のために使われるのかを明確にしておく必要があります。

下記のように、単に「3×4=12」と記述しただけでは、何のプログラムなのか記述した本人以外にはわかりません。しかも、そのプログラムを記述した本人ですら、しばらくすれば「なぜその数値を書いたのか」忘れてしまうでしょう。しかし、何も知らくてもプログラムはちゃんと動きます。まさにマジックです。

3 × 4 = 12 // 数値だけでは何のプログラムなのかわからない

ここで「3は縦の長さ」で「4は横の長さ」というように数値に意味を持たせてみましょう。「3×4=12」は「面積は3×4=12」という意味に変わり、他人にもわかるプログラムが出来上がります。

height = 3, width = 4       // 数値に意味を持たせれば

height × width = 3 × 4 = 12 // 面積を求めるプログラムだとわかる

このように、マジックナンバーは一見しただけでは何の数値なのか明確ではない数値のことを指します。ここで注意しなくてはいけないのは、即値とマジックナンバーは同じではないということです。即値とはコード (ソースファイル) 上に直接書かれた数字のことです。先ほどのプログラム場合では「3」と「4」が即値に該当します。すべての即値が必ずマジックナンバーになるわけではありませんし、逆に即値でなくても「なぜその数値なのか」という意図が明確ではない場合はマジックナンバーになってしまいます。

ということで、マジックナンバーを使うと起こり得る問題点を例示した上でその対処法を紹介します。

なお、実例としてC言語のコードで説明していますが、どの言語であっても基本的な考え方や対処法は変わりません。


マジックナンバーの例

一例として、以下のコードで説明しましょう。配列を10個用意して、配列の各要素にインデックスの数の2乗を代入していくコードです。

01 | int main(void) {

02 | int i, hairetsu[10];
03 | for (i = 0; i < 10; i++) { hairetsu[i] = i * i; }
04 | return 0;
05 | }
06 |

このうち、02行目と03行目の「10」がマジックナンバーに該当します。

マジックナンバーを理解している人はこれらの「10」が何を指しているのかわかりにくいと感じるでしょう。hairetsu[10] では「要素数が10の配列を宣言した」という意味以外が含んでおらず「10とは何なのか」という意図がコードに表れていないからです。「10」に10という数値以外の意味が含まれていないのです。

また、配列の要素数を10以外に変えたい場合、2箇所あるマジックナンバーを手打ちで書き換えなければなりません。今回のコードは短いため書き換えが容易ですが、これが何百行にもなってくると、書き換えたい数値を探すのが苦行になります。コードを一行ずつ見ていき、書き換えないといけない数値なのかをその都度判断しなければならないからです。ヒューマンエラーでうっかり書き換え損ねてしまうことも十分あり得ます。

もし、最初のマジックナンバーを「5」に書き換えて、それ以降のマジックナンバーを書き換え損ねたらどうなるでしょう?

02 | int i, hairetsu[5];

03 | for (i = 0; i < 10; i++) { hairetsu[i] = i * i; }

for文の実行中に配列の不正アクセスが発生し、セグメンテーション違反でプログラムが強制終了したり、バッファオーバーランが発生したりします。

このように、マジックナンバーは可読性が下がる問題やバグを誘発する危険性があるのです。ということで、上記のコードを改良してマジックナンバーを撤廃したコードを以下に示します。

01 | #define ARRAY_SIZE 10

02 |
03 | int main(void) {
04 | int i, hairetsu[ARRAY_SIZE];
05 | for (i = 0; i < ARRAY_SIZE; i++) { hairetsu[i] = i * i; }
06 | return 0;
07 | }
08 |

マジックナンバーだった「10」を「ARRAY_SIZE」というマクロ定数で定義して、次の行以降でそのマクロ定数を使うようにしています。

配列の要素数を変えたいときは「ARRAY_SIZE」の値を書き換えるだけで済みます。for文のループする回数も配列の要素数と同じになるため、配列の不正アクセスが起きる心配がありません。

また「ARRAY_SIZE」という名前にすることで、配列の大きさ (要素数) を表していることが類推できます。ARRAY_SIZE 10 であれば、コードに「配列の要素数を10とする」という意味を含むようになるため、可読性が上がるのです。

なお、05行目の「0」は配列の最初のインデックスを示し、数値以外の意味を含むためマジックナンバーには該当しません。 (return 0; の「0」も同様です)

このように、数値以外の意味を含んでいない即値や、あとで変更される可能性のある数値を、意味を持たせたマクロ定数に書き換えることで、コードの可読性を上げて修正や変更を容易にしたり、バグの発生を未然に防いだりすることができるのです。これが、マジックナンバーをなくす常套手段です。


マジックナンバーではなくマクロ定数に直すべきでない例

逆に、マジックナンバーではない値をわざわざマクロ定数に書き換えるのは却って可読性を下げてしまうことがあります。次のコードを見てみましょう。

01 | #include <stdio.h>

02 | #define TO_HALF 2
03 |
04 | int main(void) {
05 | double width, height, area;
06 | width = 3.0;
07 | height = 2.5;
08 | area = width * height / TO_HALF; /* 2 から TO_HALF に変更 */
09 | printf("底辺:%f 高さ:%f 面積:%f \n", width, height, area);
10 | return 0;
11 | }
12 |

三角形の面積を計算して printf() で表示するコードです。言うまでもなく、三角形の面積の公式は以下の通りです。


三角形の面積 = 底辺 × 高さ ÷ 2


このうちの「2」は即値であるため TO_HALF という「半分にする」ためのマクロ定数を定義して公式に適用しています。

しかし、TO_HALF が半分にするマクロ定数であることは名前で何となくわかりますが、一見しただけでは「2で割って半分にする」のか「0.5を掛けて半分にする」のかまではわかりません。つまり、こんなコードも考えられる訳です。

01 | #include <stdio.h>

02 | #define TO_HALF 0.5
03 |
04 | int main(void) {
05 | double width, height, area;
06 | width = 3.0;
07 | height = 2.5;
08 | area = width * height * TO_HALF; /* TO_HALF は 0.5 ? */
09 | printf("底辺:%f 高さ:%f 面積:%f \n", width, height, area);
10 | return 0;
11 | }
12 |

こうなってくると、TO_HALF の値をわざわざ確認する必要があります。

一部のIDEやコードエディタではマクロ定数をツールチップで数値を表示する機能があるため、(わかりにくいとは言え) あまり問題にならないと思いますが、ほとんどのテキストエディタではそのような機能はないため非常に煩わしいでしょう。

そこまでするなら、数式上にある「係数」は即値で表記するべきです。

01 | #include <stdio.h>

02 |
03 | int main(void) {
04 | double width, height, area;
05 | width = 3.0;
06 | height = 2.5;
07 | area = width * height / 2; /* 三角形の面積の算出 */
08 | printf("底辺:%f 高さ:%f 面積:%f \n", width, height, area);
09 | return 0;
10 | }
11 |

一見しただけで三角形の公式を使用していることが明確ですし、マクロ定数の中身をわざわざ確認するという余計な作業をする必要がありません。もっとも、上記のコード上にある「2」は三角形の面積の公式に出てくる不変の値であって、マジックナンバーではありません。そのため、わざわざマクロ定数に書き換えるのは、メリットがないどころか混乱の元になってしまいます。

このように、適切なコードを書くには 即値がマジックナンバーかどうかを適切に判断する必要がある のです。もし即値を使っていて、それがマジックナンバーに該当するか不安に感じたら、上記のコードのようにコメントを書き足すと良いでしょう。


マジックナンバーではないがマクロ定数に直した方が良い例

先ほどの例とは逆に、マジックナンバーではなくてもマクロ定数に直した方が良い例もあります。次のコードを見てみましょう。

01 | #include <stdio.h>

02 |
03 | int main(void) {
04 | double radius, area;
05 | radius = 3.0;
06 | area = radius * radius * 3.14; /* 円周率を 3.14 として計算 */
07 | printf("半径:%f 面積:%f \n", radius, area);
08 | return 0;
09 | }
10 |

円の面積を計算して printf() で表示するコードです。即値に該当するものは、3.03.14 そして main() 末尾の 0 の3つです。

3.0 は半径を指す変数 radius に代入しており、3.14 は円周率を指すため、マジックナンバーに該当する即値はありません。ちなみに、最近の学習塾では円周率を「3」と教えていた時期があったとか。

しかし、手計算で行う分には円周率の有効桁数が3桁だけでも十分かもしれませんが、コンピューターで計算する上では精度が不十分です。double型 (8バイト・64ビット・IEEE 754 に準拠と仮定) の有効桁数は約15桁ですので、無理数である円周率を 3.141592653589793 まで表記しておきたいところですが、円周率を使う度に入力するのは大変ですし、わざわざ覚えていられないでしょう。それなら、円周率を即値からマクロ定数に替えてしまいましょう。

01 | #include <stdio.h>

02 | #define PI 3.141592653589793
03 |
04 | int main(void) {
05 | double radius, area;
06 | radius = 3.0;
07 | area = radius * radius * PI; /* 円周率を PI に書き換え */
08 | printf("半径:%f 面積:%f \n", radius, area);
09 | return 0;
10 | }
11 |

3.141592653589793 と入力するよりも PI で入力した方がタイプ量を大幅に減らせます。また、数学の世界では円周率を π で表記するため、こちらの方が自然に感じる人が多いでしょう。このように、値そのものは自明ではあるものの有効桁数が多い場合は、定数で定義することも検討しましょう。

ちなみに、コンパイラーの中には <math.h> (C++では <cmath>) に円周率 M_PI をマクロ定数として定義しているものもあります。ただし、標準ライブラリには存在しないマクロ定数であるため、先ほどのコードのように手動で定義した方が無難です。なお、Visual C++ では _USE_MATH_DEFINES というマクロを定義しないと、M_PI が使えないように制限されています。

01 | #include <stdio.h>

02 | #define _USE_MATH_DEFINES /* Visual C++ ではこれが必要 */
03 | #include <math.h>
04 |
05 | int main(void) {
06 | double radius, area;
07 | radius = 3.0;
08 | area = radius * radius * M_PI; /* これで M_PI が使える */
09 | printf("半径:%f 面積:%f \n", radius, area);
10 | return 0;
11 | }
12 |


マクロ定数がマジックナンバーになっている悪い例

最後に「マジックナンバーを撤廃する」目的や意義を理解せずにとんでもないコードを書いた例を紹介します。間違ってもこんなコードを書いてはいけませんよ? (下記のリンクを参考にしてコードを再現しています)

参考:マジックナンバーへの対処法: つれづれネット散歩

01 | #define INTEGER_ZERO    0

02 | #define INTEGER_FIVE 5
03 | #define INTEGER_HUNDRED 100
04 |
05 | int main(void) {
06 | int i;
07 | double tax, amount = 314;
08 | for(i=INTEGER_ZERO; i < INTEGER_FIVE; i++) {
09 | /* 何かしらの処理 */
10 | }
11 | tax = (amount * INTEGER_FIVE) / INTEGER_HUNDRED; /* 消費税計算? */
12 | return 0;
13 | }
14 |

これはひどい…… 即値をマクロ定数に書き換えただけ!

「さすがにこれは冗談だろ?」と思ったのですが、実際のソフトウェア開発でもこのようなコードを見かけるそうです。

先ほどの例でも説明しましたが、もしfor文のループ回数を変えたいときは、その数に応じて08行目の INTEGER_FIVEINTEGER_TENINTEGER_FIFTY などに書き換えることになります。これはマクロ定数に数値以外の意味が含まれていないために起きる問題で、即値で書いた場合と全く変わりません。新たな整数を使う度にマクロ定数を定義しなくてはならないのも苦行でしょう。また11行目では、百分率を求めるためにわざわざ INTEGER_HUNDRED を使うのは不自然ですし、消費税率が変わった際には INTEGER_FIVE を書き換える必要もあります。

マジックナンバーの対策になっていないどころか、マクロ定数そのものがマジックナンバーになってしまっているのです。

余談ですが、マクロ定数の先頭が型名を表していることから「システムハンガリアン記法」を彷彿させます。

少なくとも、数値以外の意味が含まれていないマクロ定数は定義してはなりません。正しく書き換えるとこのようになるでしょう。

01 | #define TAX_RATE 8 /* 消費税率は 5% から 8% に上がりました (2017年12月現在) */

02 |
03 | int main(void) {
04 | int i;
05 | double tax, amount = 334;
06 | int loop_count = 5; /* ループする回数はここで指定 */
07 | for(i = 0; i < loop_count; i++) {
08 | /* 何かしらの処理 */
09 | }
10 | tax = amount * TAX_RATE / 100.0; /* 消費税を算出する処理 */
11 | return 0;
12 | }
13 |

なぜかamountの値が変わっている? こまけぇこたぁいいんだよ!!


要点

マジックナンバーについていろいろ書いてきましたが、最後に要点だけをまとまておきましょう。


  • マジックナンバーは数値そのものの意味以外を含んでいない数値やマクロ定数のことである

  • 数値の意味がわからなくてもプログラムは問題なく動くため「まさにマジックだ」と皮肉って命名された

  • コードにマジックナンバーを含んでいるとバグを誘発したりコードが読みにくくなったりする

  • 即値とマジックナンバーは似て非なるものでありコード上の数値がどちらに該当するか見極める必要がある

  • 数式の係数なら「即値」無理数なら「定数」で記述すると良い

  • 数値以外の意味を含まないマクロ定数や変数は定義してはいけない