浦安市在住+デジカメ
浦安市在住のプログラマーが、デジカメを片手に日々を語ったり語らなかったり愚痴ったり・・・
-
デバッガーって、いったいなんなのさ
Posted on 10月 17th, 2017 はおりん No commentsプログラムを開発していると必ず通る道、それがデバッグです。デバッグ、またはデバッグ作業とはなんなのか時として「デバッグ」と呼ばれ、時には「バグ出し」と呼ばれることもありますが、基本的には「bug」を「de」するものであるので、「バグ取り」作業であります。「バグ潰し」なんて呼ばれたりもします。さて、バグをつぶすには、そのバグの発生源を見つけなければなりません。バグは自然発生はしませんから、かならず人が埋め込んだものであるので、プログラムのどこかに何かの間違いがあります。例えば、商品の税抜き価格を合計して、深夜料金を足して、全体に消費税をかけると合計金額になる。というプログラムがあったとします。var Const = {"midnightCheck": 250,"taxRate": 0.08}function calc(orders, isMidnight) {// 変数初期化var check = 0;var price = 0;var quantity = 1;for (var i in orders) {// 単価を取得price = orders[i].item.price;// 数量がセットされている場合には数量も取得if (orders[i].quantity) {quantity = orders[i].quantity;}// 単価 x 数量 を合計金額に足すcheck += price * quantity;}// 深夜料金を加算if (isMidnight) {check += Const.midnightCheck;}// 全体に消費税を足すcheck = check + (check * Const.taxRate);return check;}var orders = [{ "item": { "name": "100円の商品を1つ", "price": 100 } },{ "item": { "name": "80円の商品を2つ", "price": 80 }, "quantity": 2 },{ "item": { "name": "110円の商品を1つ", "price": 110 } },];console.log(calc(orders, true));そうそう、この記事では言語としてJavaScriptを使用していますが、お手元のブラウザで実行できて便利というだけで採用しているのであって、デバッガはたいていどの言語にもあります。なお、プログラムの実行結果は必ず掲載しますので、わざわざお手元の環境で実行しなくても読み進めることは出来ます。さて、このプログラムを実行してみましょう。脳内実行でバグを見つけ出せた人には簡単に感じるかもしれないけれど、サンプルなので許していただきたい。あとコードが美しくないのは敢えてエンバグするためです。肝心の実行結果は、こうなります。788.4さて、この結果は合っているだろうか?もちろん合ってません。この記事はデバッグについて紹介する記事なので、合っているわけがありません。実際に電卓で叩いてみましょう。このプログラムを人間の文章で表すと、こうなります。100円の商品を1つと、80円の商品を2つと、110円の商品を1つ買い、その小計と深夜料金の合計金額に、消費税をかける。関数電卓に入力できる式にすると、こうです。このままコピペできます。Google先生にもコピペできます。(100 + (80 * 2) + 110 + 250) * 1.08結果は669.6となりました。なんと困ったことに118.8円も多く取ってしまいました。深夜料金の時点ですでにボッタクリなのに、これでは詐欺もいいところです。金返せ!
さて、と。単体テストの結果、このプログラムにバグがあることが判明しました。しかしどこが間違っているのか、イマイチわかりません。わからないことにしてください。わからないので、途中経過を何カ所か表示してみましょう。print文(JSならconsole文)の入れ方は人それぞれあると思いますが、わたしは最終的に「消し忘れる」のがイヤなので、インデント無しで入れることにしています。結果、こういうコードになりました。var Const = {"midnightCheck": 250,"taxRate": 0.08}function calc(orders, isMidnight) {// 変数初期化var check = 0;var price = 0;var quantity = 1;for (var i in orders) {console.log(orders[i].item.name);// 単価を取得price = orders[i].item.price;console.log(price);// 数量がセットされている場合には数量も取得if (orders[i].quantity) {quantity = orders[i].quantity;console.log("quantity");console.log(quantity);}// 単価 x 数量 を合計金額に足すcheck += price * quantity;console.log(check);}// 深夜料金を加算if (isMidnight) {check += Const.midnightCheck;console.log("midnight");console.log(Const.midnightCheck);console.log(check);}// 全体に消費税を足すcheck = check + (check * Const.taxRate);console.log("tax");console.log(Const.taxRate);console.log(check);return check;}var orders = [{ "item": { "name": "100円の商品を1つ", "price": 100 } },{ "item": { "name": "80円の商品を2つ", "price": 80 }, "quantity": 2 },{ "item": { "name": "110円の商品を1つ", "price": 110 } },];console.log(calc(orders, true));これを実行すると、こんなログが出てきます。(表示のされ方はブラウザによって違うと思いますが。)VM260:13 100円の商品を1つVM260:16 100VM260:27 100VM260:13 80円の商品を2つVM260:16 80VM260:21 quantityVM260:22 2VM260:27 260VM260:13 110円の商品を1つVM260:16 110VM260:27 480VM260:33 midnightVM260:34 250VM260:35 730VM260:40 taxVM295:41 0.08VM295:42 788.4VM295:53 788.4undefined読みにくっ…!1つ1つ読み解いてみましょう。// —–> ここから1つめの商品// 商品の名前と単価VM260:13 100円の商品を1つVM260:16 100// 現在の合計金額VM260:27 100// <—– ここまで1つめの商品// —–> ここから2つめの商品// 商品の名前と単価VM260:13 80円の商品を2つVM260:16 80// 数量が2VM260:21 quantityVM260:22 2// 現在の合計金額VM260:27 260// <—– ここまで2つめの商品// —–> ここから3つめの商品// 商品の名前と単価VM260:13 110円の商品を1つVM260:16 110// 現在の合計金額VM260:27 480// <—– ここまで2つめの商品// 深夜料金を加算するVM260:33 midnightVM260:34 250// 深夜料金加算後の合計金額VM260:35 730// 消費税をかけるVM260:40 taxVM260:41 0.08VM260:42 788.4// 最終的な結果VM260:53 788.4// 実行したコード全体として戻り値がありませんでしたという意味で、無視して良いundefinedこれだけいろいろ出してみれば、どこで間違ったか分かりそうです。計算過程を下の表にまとめました。下線が引いてある部分が「check変数」に相当する部分です。計算する内容正しい結果プログラムの実行過程100円の商品を1つ0 + 100 = 1000 + 100 = 10080円の商品を2つ100 + (80 x 2) = 260100 + (80 x 2) = 260110円の商品を1つ260 + 110 = 370260 + 110 = 480深夜料金を加算する370 + 250 = 620480 + 250 = 730消費税をかける620 + (620 x 0.08) = 669.6730 + (730 x 0.08) = 788.4「110円の商品を1つ」の部分で間違っていることがわかりました。260+110はどう考えても480にはなりません。というわけでここらへんを重点的に調べてみます。var Const = {"midnightCheck": 250,"taxRate": 0.08}function calc(orders, isMidnight) {// 変数初期化var check = 0;var price = 0;var quantity = 1;for (var i in orders) {console.log(orders[i].item.name);// 単価を取得price = orders[i].item.price;console.log(price);// 数量がセットされている場合には数量も取得console.log(orders[i].quantity);if (orders[i].quantity) {quantity = orders[i].quantity;console.log("quantity");console.log(quantity);}// 単価 x 数量 を合計金額に足すconsole.log(price * quantity);check += price * quantity;console.log(check);}// 深夜料金を加算if (isMidnight) {check += Const.midnightCheck;}// 全体に消費税を足すcheck = check + (check * Const.taxRate);return check;}var orders = [{ "item": { "name": "100円の商品を1つ", "price": 100 } },{ "item": { "name": "80円の商品を2つ", "price": 80 }, "quantity": 2 },{ "item": { "name": "110円の商品を1つ", "price": 110 } },];console.log(calc(orders, true));実行するとこういうログになります。解説も入れておきます。// —–> ここから1つめの商品// 商品の名前と単価VM274:11 100円の商品を1つVM274:14 100// 数量VM274:16 undefined// 単価 x 数量VM274:23 100// 現在の合計金額VM274:25 100// <—– ここまで1つめの商品// —–> ここから2つめの商品// 商品の名前と単価VM274:11 80円の商品を2つVM274:14 80// 数量VM274:16 2VM274:19 quantityVM274:20 2// 単価 x 数量VM274:23 160// 現在の合計金額VM274:25 260// <—– ここまで2つめの商品// —–> ここから3つめの商品// 商品の名前と単価VM274:11 110円の商品を1つVM274:14 110// 数量VM274:16 undefined// 単価 x 数量VM274:23 220 // <<< 数量指定が無いのに、単価 x 数量が220円になっている// 現在の合計金額VM274:25 480// <—– ここまで3つめの商品VM274:40 788.4110円の商品を1つのところで、小計が220円になっているということは、数量が2で計算されているということです。ログを見ても単価は合っています。間違っているのは明らかに数量です。いったいどこで間違ったのでしょう。。。var Const = {"midnightCheck": 250,"taxRate": 0.08}function calc(orders, isMidnight) {// 変数初期化var check = 0;var price = 0;var quantity = 1;console.log(quantity);for (var i in orders) {console.log(orders[i].item.name);// 単価を取得price = orders[i].item.price;// 数量がセットされている場合には数量も取得console.log(orders[i].quantity);if (orders[i].quantity) {quantity = orders[i].quantity;console.log("quantity");}console.log(quantity);// 単価 x 数量 を合計金額に足すcheck += price * quantity;}// 深夜料金を加算if (isMidnight) {check += Const.midnightCheck;}// 全体に消費税を足すcheck = check + (check * Const.taxRate);return check;}var orders = [{ "item": { "name": "100円の商品を1つ", "price": 100 } },{ "item": { "name": "80円の商品を2つ", "price": 80 }, "quantity": 2 },{ "item": { "name": "110円の商品を1つ", "price": 110 } },];console.log(calc(orders, true));quantity周りを重点的に追ってみます。// quantityの初期値VM292:10 1// —–> ここから1つめの商品VM292:12 100円の商品を1つ// 数量指定は無しVM292:16 undefinedVM292:21 1// <—– ここまで1つめの商品// —–> ここから2つめの商品VM292:12 80円の商品を2つ// 数量指定は2VM292:16 2VM292:19 quantityVM292:21 2// <—– ここまで2つめの商品// —–> ここから3つめの商品VM292:12 110円の商品を1つ// 数量指定は無しVM292:16 undefinedVM292:21 2 // <<< 数量指定は無しなのに、数量が2になっている// <—– ここまで3つめの商品VM292:38 788.4そうです、「80円の商品を2つ」の部分で使った、quantity変数を初期化していないために、1つ前の商品の数量が残ったままになってしまっていたのがバグの原因でした。デ バ ッ グ 完 了
ふう、疲れました。大量のconsole文の力の前にquantityなどというちっぽけな変数はあまりに無力でした。圧倒的戦力差だったと言わざるをえません。しかしなんという物量戦でしょう。今はまだちっぽけな戦場<プログラム>ですが、もっと戦域が拡大してしまったら有象無象のconsole文に軍司令部もコントロールを失ってしまうでしょう。実際、合計行数が何万行もあり、ファイルも数百個あり、メソッド数も数万に及ぶような歴史あるプロジェクトでは、print文でデバッグを行うのは大変な労力となります。そこで登場する新兵器が「デバッガー」です。デバッガーで何が出来るというのでしょう?まさかconsole文を書く必要が無くなるとか言うんじゃないでしょうか?そのまさかです。デバッガーには、たいてい、以下の機能があります。・コードを1行ずつ実行する。1行実行するたびに一時停止する。(ステップ実行)・一時停止中に変数の中を見たり、書き換えたりする。・任意の箇所でコードを一時停止する。他にもいろいろ機能はありますが、最低限これだけ押さえていれば大丈夫です。デバッガーなんて、どこにあるのでしょう?↑のJavaScriptをデバッグするツールなんて、いかにも高そうです。稟議を通さないといけません。上長の許可とハンコが必要です。大変です。でも安心してください。みんな大好きChromeには、ものすごく立派なJavaScriptの(HTMLとCSSのデバッグにも使える)デバッガーが載っています。その名も「Developer Tool」です。
ここからはChromeのお時間です。みなさま、Chromeを起動してください。あとテキストエディタも必要です。まず下記のHTMLを適当なファイル名で保存してください。わたしは `debugger.html` にしました。<html><head><title>デバッガをデバッガでテストするためのデバッガ(イミフメイ)</title><script type="application/javascript">var Const = {"midnightCheck": 250,"taxRate": 0.08}function calc(orders, isMidnight) {// 変数初期化var check = 0;var price = 0;var quantity = 1;for (var i in orders) {// 単価を取得price = orders[i].item.price;// 数量がセットされている場合には数量も取得if (orders[i].quantity) {quantity = orders[i].quantity;}// 単価 x 数量 を合計金額に足すcheck += price * quantity;}// 深夜料金を加算if (isMidnight) {check += Const.midnightCheck;}// 全体に消費税を足すcheck = check + (check * Const.taxRate);return check;}var orders = [{ "item": { "name": "100円の商品を1つ", "price": 100 } },{ "item": { "name": "80円の商品を2つ", "price": 80 }, "quantity": 2 },{ "item": { "name": "110円の商品を1つ", "price": 110 } },];console.log(calc(orders, true));</script></head><body>なにもないよ</body></html>このファイルをChromeで開くと、「なにもないよ」と表示されたシンプルな画面になります。このままでは何も出来ないので、デベロッパーツールを起動しましょう。メニューのここにあります。
【蛇足】Chromeのデベロッパーツールの機能デベロッパーツールには、いくつかの機能があります。左側から見ていきましょう。HTML構造やCSSの内容をいじることができます。ここでいじった内容はリアルタイムにブラウザの画面に反映されます。Elementsに表示されているHTMLの各要素にマウスオーバーすると、その要素がブラウザ画面でハイライトされます。ConsoleタブでJavaScriptを直接実行することができます。コンテキスト(実行されるコードの存在する領域)は「window」です。window以下に存在する変数、JavaScript版のグローバル変数とも言うべき変数に直接アクセスし、中身を見たり書き換えたりできます。JavaScriptやcssのソースを直接見ることができます。ブラウザが通信している内容をキャプチャできます。ここでHTTP通信の内容におかしな点が無いかを確認します。
さて、ChromeのデベロッパーツールにおけるJavaScriptのデバッガーは「Sources」タブです。Sourcesタブを開くと、テキストファイルの内容が表示されていると思います。ちゃんとシンタックスハイライトもされているはずです。それではこの画面の25行目の行番号付近「25」と表示されている部分をクリックしてください。こんな感じになったら、ブラウザをリロードしてみましょう。きちんと25行目で止まってくれたでしょうか?プログラムの実行中に任意の箇所で一時停止する機能を「ブレークポイント」と言います。一時停止しているプログラムを、1行ずつ実行していくことも出来ます。Sourcesタブの、右上のほうにあるボタンで、一時停止中のプログラムを制御できます。プログラムを再開します。再びブレークポイントを通過すると一時停止します。現在の行を実行して、次の行に進みます。現在の行を実行して、次の行に進みます。現在の行が関数やメソッドの場合、その内部の先頭の行で一時停止します。Step overはメソッドの中を見ません。Step intoはメソッドの中を見ます。現在の行がある関数・メソッドから出て、呼び出し元に戻るまで実行します。25行目でStep outすると、37行目まで飛んでいくのが見えると思います。これは37行目でcalcメソッドを呼んでいるからです。
デバッガは、変数を書き換えることが出来ます。え? window.aaa = "AAA" って打ち込んで書き換えれば良い?いやいや、ブレークポイントで停止した時点の、そのスコープで見えている任意の変数を書き換えることが出来るのです。ローカル変数を書き換えたり、コードを1行ずつ実行しながらリアルタイムに変化を追うことが出来ます。では、再度ページをリロードして、もういちど25行目で止めてみましょう。右側にあるパネルの「Scope」の部分が、現在のスコープで見えている変数です。isMidnight はメソッドの引数です。メソッドの引数も、扱いはローカル変数と変わらないので書き換えることが可能です。(ただし引数は参照渡しされていることがあるので注意しましょう。)さて、では実際に、isMidnightを書き換えてみましょう。まずは1度、書き換えずに実行します。では次に、ブレークポイントで停止している間にisMidnightをfalseに変えてみます。結果が変わりました。計算する内容isMidnight = trueidMidnight = false100円の商品を1つ0 + 100 = 1000 + 100 = 10080円の商品を2つ100 + (80 x 2) = 260100 + (80 x 2) = 260110円の商品を2つ(バグを含む)260 + (110 x 2) = 480260 + (110 x 2) = 480深夜料金を加算する480 + 250 = 730480 + 0 = 480消費税をかける730 + (730 x 0.08) = 788.4480 + (480 x 0.08) = 518.4
では、実際にバグを探してみましょう。当初、こういったデバッグ用コードを書きました。var Const = {"midnightCheck": 250,"taxRate": 0.08}function calc(orders, isMidnight) {// 変数初期化var check = 0;var price = 0;var quantity = 1;for (var i in orders) {console.log(orders[i].item.name);// 単価を取得price = orders[i].item.price;console.log(price);// 数量がセットされている場合には数量も取得if (orders[i].quantity) {quantity = orders[i].quantity;console.log("quantity");console.log(quantity);}// 単価 x 数量 を合計金額に足すcheck += price * quantity;console.log(check);}// 深夜料金を加算if (isMidnight) {check += Const.midnightCheck;console.log("midnight");console.log(Const.midnightCheck);console.log(check);}// 全体に消費税を足すcheck = check + (check * Const.taxRate);console.log("tax");console.log(Const.taxRate);console.log(check);return check;}var orders = [{ "item": { "name": "100円の商品を1つ", "price": 100 } },{ "item": { "name": "80円の商品を2つ", "price": 80 }, "quantity": 2 },{ "item": { "name": "110円の商品を1つ", "price": 110 } },];console.log(calc(orders, true));これを実行すると、こんなログが出てきました。VM260:13 100円の商品を1つVM260:16 100VM260:27 100VM260:13 80円の商品を2つVM260:16 80VM260:21 quantityVM260:22 2VM260:27 260VM260:13 110円の商品を1つVM260:16 110VM260:27 480VM260:33 midnightVM260:34 250VM260:35 730VM260:40 taxVM295:41 0.08VM295:42 788.4VM295:53 788.4undefinedえぇ、読みにくかったです。はい、とても読みにくかったです。これをデバッガで実行すると、こうなります。このGIFアニメはやや速いので、実際にご自身で1行ずつ実行しながら、変数の値を確認してみてください。おそらくすぐにquantityのバグに気付けると思います。
まとめ・よくわからないバグは1行ずつ実行して検証しましょう。・変数の正しい状態を脳内でシミュレートして、実際の値と比較してみましょう。・最初はステップオーバーで実行して、怪しいと感じたメソッドをステップインで深掘りしましょう。
※注釈・シンタックスハイライトには http://markup.su/highlighter/ を使わせていただきました。
プログラミングコメントを書く