詰まったバグも即解決! 簡単にデバッグする3つの方法
目次
バグをいかにハントし、食卓に並べるか
バグに捕まっている皆さん、こんにちは。narupoです。
またはあなたは日々の研鑽(けんさん)を怠らない狩人かもしれません。
この記事にたどり着いたということは、あなたは以下のことを望んでいるはずです。
高速でバグを解決したい
デバッグ能力を上げたい
プログラミングでバグが発生した際に、それらを解決することをデバッグと言います。
この記事では以下のデバッグ方法を取り上げています。
伝家の宝刀、printfデバッグ
デバッガ―を使う
二分探索でデバッグの効率を上げる
この記事を読めばあなたも今日からデバッグ名人です。
バグの急所を見極め、ひと突きでバグを仕留める歴戦の狩人になれると言えるでしょう。
(その場合、夕食はバグになるわけだけど、さすがにバグは食えない)
では奥深きデバッグの世界へ、ようこそ。
デバッグの効率を上げなければいけない理由2つ
デバッグの効率を上げたい理由はいくつか挙げられます。
時間を節約したい
精神的疲労を少なくしたい
皆さんも思い当たるふしがあるのではないでしょうか。
時間を節約したい
デバッグは開発者の時間を吸い取っていきます。
それこそ食欲旺盛な新製品の掃除機のように。
デバッグで1時間、2時間、または1日、2日…、1週間、時間を潰したなんてことはザラに聞く話です。
それほど厄介なデバッグというものは時間がかかるものです。
特に原因が不明なバグだと、その手がかりをつかむのは困難と言えます。
この手がかりを掴むために我々は「武器」を使う必要があります。
精神的疲労を少なくしたい
デバッグは開発者の精神力を削り取っていきます。
長期間にわたりデバッグを担当した開発者は発狂し、この世のものとは思えない醜態を晒すことになります。
それほどデバッグとは開発者のメンタルヘルスを悪化させるのです。
私たちは健全な社会生活を送るためにもメンタルヘルスを維持する必要があります。
恋人と一緒に過ごしたり、猫を撫でたり、ゲームをやったり、ネットサーフィンをやったりして、精神力を蓄えるのです。
もっとも、最初の一文は私には縁遠い話ですが ( ;ω; )ゥッ…。
しかしそれでも度重なるデバッグによって精神は疲労していきます。
この疲労を少なくするために我々は「二分探索」を使うのです。
デバッグのお供! ご機嫌なメンバー3人
それでは皆さんにデバッグで役立つご機嫌なメンバーを紹介しましょう。
すでに顔なじみの方もいるかもしれませんね。
伝家の宝刀、printfデバッグ
まず最初に紹介するのが伝家の宝刀ともいえる「printfデバッグ」です。
これは、あらゆるジャンルのプログラミングで使われる、最も一般的なデバッグです。
具体的にはコードの要素要素にprintfをはさみ、その前後のプログラムの状態を出力します。
その出力を見て開発者はプログラムのバグを突き止めていきます。
ちなみに「printf」とはC言語の関数が由来です。
ここで有名なC言語のHello, World!プログラムを見てみましょう。
#include <stdio.h>
int main(void) {
printf("Hello, World!\n");
return 0;
}
言語によってはprintfよりもechoとかprintとかのほうが一般的かもしれませんね。
デバッグにおける役割はどれも同じで、目的はプログラムの状態を出力することです。
printfデバッグのコツ
printfデバッグのコツは、メッセージをきちんと書くことです。
最初にサンプルとなるプログラムを見てみましょう。
#include <stdio.h>
void run(void) {
const char *cmdname = read_command_name();
execute_command_by_name(cmdname);
}
この短いプログラムのサンプルはC言語で書かれています。
コマンド名を読み込み、そのコマンド名からコマンドを実行する簡単なプログラムです。
しかし、何らかの原因でバグが発生し、コマンドが実行されない現象が起こりました。
さっそく私たちはこのサンプルをprintfデバッグしたいと思いました。
コマンドが実行されないということは、実行するコマンド名がおかしい可能性があります。
そこで私たちは
#include <stdio.h>
void run(void) {
const char *cmdname = read_command_name();
puts(cmdname); // ← コマンド名を確認するためprintfデバッグを加えた
execute_command_by_name(cmdname);
}
このようにコマンド名を確認するprintfデバッグを仕込みました。
しかし、これはあまり良いデバッグではありません。
なぜなら出力されている文字列が何なのか、このコードからではわからないからです。
より良いprintfデバッグを行うには
#include <stdio.h>
void run(void) {
const char *cmdname = read_command_name();
printf("cmdname: [%s]\n", cmdname); // ← 改善したprintfデバッグ
execute_command_by_name(cmdname);
}
このようにすると良いでしょう。
先ほどのprintfデバッグと比べて、出力されている文字列が何の文字列かわかるという点と、[]を加えることで半角スペースなども視覚的に捕捉できるようになっています。
もちろん、このprintfデバッグがいつもベストとは限りません。
なぜなら先ほどの前者のprintfデバッグのほうが時間を節約できることもあるからです。
しかし、デバッグでは時間がかかることのほうが一般的です。
その場合、重宝されるのは後者のprintfデバッグと言えるでしょう。
printfデバッグにメッセージを加えることで、結果的に時間を節約できるようになるかもしません。
デバッガ―を使う
デバッガーを使うことで動作中のプログラムの中身を見ることが出来るようになります。
デバッガーを使いこなすことができればデバッグの効率も上がることでしょう。
printfデバッグで時間がかかりそうだなと感じたら、デバッガーに切り替えるといいでしょう。
デバッガーはリアルタイムでデバッグが可能なので、その効率はprintfデバッグの比になりません。
IDE(統合開発環境)によってはデバッガーが付属している場合もあります。
近代的なIDEのデバッガーを使うことでデバッグの効率を上げられることでしょう。
しかし、場合によってはprintfデバッグのほうが効率的な場合もあります。
使いどころをよく考えて適したツールを選択するのは開発者の経験によります。
一般に最初はprintfデバッグを試し、やっかいなバグだったらデバッガーに切り替えるのがいいでしょう。
有名なデバッガーではC言語のgdb, Pythonのpdbなどがあります。
二分探索でデバッグの効率を上げる
最後にご紹介したいのが「二分探索」です。
この二分探索を知っているかどうかでデバッグの効率が格段に違います。
二分探索は探索のアルゴリズムの一種で、英語ではバイナリーサーチと言います。
この探索の特徴は、探索対象を二分割しながら探索していくことです。
こうすることでコストの高い探索を現実的なコストにまで引き下げることが可能です。
具体例を見てみましょう。
以下のコードは簡単なC言語のサンプルです。
#include <stdio.h>
void setup(void) {
setup_database();
setup_controllers();
setup_views();
setup_models();
}
DBの設定とMVCの設定を行っています。
私たちはこの設定のどこかで処理が停止するバグに捕まりました。
どこで処理が止まっているかを確かめるため、printfデバッグを行うことにしました。
#include <stdio.h>
void setup(void) {
setup_database();
puts("done setup database")
setup_controllers();
setup_views();
setup_models();
}
最初にDBの設定が完了しているか確かめるため、printfデバッグを挟みました。
しかしこれは、よくないデバッグ例です。
なぜなら二分探索的ではないからです。
もし挟んだ箇所でバグが特定できなかった場合、すべての設定においてprintfデバッグを挟まなければいけない可能性が出てきます。
例えば以下のような状態です。
#include <stdio.h>
void setup(void) {
setup_database();
puts("done setup database")
setup_controllers();
puts("done setup controllers")
setup_views();
puts("done setup views")
setup_models();
puts("done setup models")
}
ここまでやってやっと私たちはモデルの設定がうまく行ってないことに気が付きました。
しかしこれは、明らかに時間の無駄です。
二分探索的なアプローチではまず最初に以下のように行います。
#include <stdio.h>
void setup(void) {
setup_database();
setup_controllers();
puts("done setup controllers")
setup_views();
setup_models();
}
設定処理は全部でDBとMVCの4つです。
そこで設定処理の中間にあたるコントローラーの設定後にprintfデバッグを挟みました。
その結果、コントローラーの設定は上手くいっていることが判明しました。
次に私たちは以下のようにprintfデバッグを挟みます。
#include <stdio.h>
void setup(void) {
setup_database();
setup_controllers();
setup_views();
puts("done setup views")
setup_models();
}
コントローラーの後の設定処理は全部でビューとモデルの二つです。
そこでビューとモデルの中間にprintfデバッグを挟みます。
この結果、ビューの設定もうまく行っていることが判明しました。
最後に私たちは以下のようにprintfデバッグを挟みます。
#include <stdio.h>
void setup(void) {
setup_database();
setup_controllers();
setup_views();
setup_models();
puts("done setup models")
}
こうして私たちはモデルの設定がうまくいっていないことを理解しました。
最初のprintfデバッグではprintfを挟む回数が4回だったのに対して、二分探索では3回になっています。
-1回分のコストダウンになります。
今回は最大で4回分のコストでしたが、もしこれが最大で10回分のコストだったらどうなるでしょうか?
その結果はなんと、二分探索では最もコストがかかる場合でも4回で済みます。
二分探索というアルゴリズムの恩恵によって、デバッグのコストを現実的な値にまで減らすことができます。
すばらしいですね。
デバッグを愛することで私たちは幸せになれた
最後に、よくバグに調教されたプログラマーの言葉をお送りしたいと思います。
「俺はバグが嫌いだった。見るのも嫌なぐらいさ。だが、何年も何年もバグと向き合う内に、いつしかデバッグが楽しくなってきたんだ。自分でも何を言ってるかわからねぇが、これは本当なんだ――」
デバッグとは通常、辛く苦しい作業です。
しかし、ある境を超えたとき、その作業は一種のゲームのように楽しくなって来ると言えます。
そこまでの境地に達している開発者は決して多くはありませんが、私たちもいつしかその境地に達したいものですね。