今回は、初心者がプログラミングをするときに、つまづきやすいエラーについての話をします。
プログラミングをしていると、確実にエラーに遭遇するでしょう。
エラーが出ると、気が滅入り、モチベーションも下がってしまいますね。
本記事では、初心者が最低限覚えておきたい以下の3種類のエラーについて説明します。
- ビルドエラー
- 実行時エラー
- 論理エラー
また、エラー原因を特定する作業(デバッグ)において重要となる「仮説検証」の考え方と、以下の代表的なデバッグ方法2つについても説明します。
- ブレークポイントデバッグ
- printfデバッグ
この記事を読むことで、プログラムを書いていてエラーに遭遇したときに以下がわかります。
- どのような種類のエラーなのか
- そのエラーにどのような考え方で対処していけばよいか
これらのエラーやデバッグ方法について、演習で具体例を見ながら一緒に学んでいきましょう。
C#の基本(変数、型、演算子、制御構文)については、入門編(1)~(5)を参考にください。
YouTubeの動画でも解説しているので、ぜひ御覧ください。Visual Studioの実際の操作などについて、こちらの方がわかりやすいかと思います。
演習1:プログラムのエラーを修正する
今回は、前回の演習でも使った、以下の仕様のプログラムコード作成を想定します。
- 整数が並んだデータ(整数配列)における要素番号とその要素値を、HTMLの表形式で出力する。
- 整数配列の全要素の平均値についても出力する。
配列データとして[1,2,3,4,5]という5つの要素があった場合、以下のようにブラウザで表示するHTMLコードを出力するのが、完成イメージとなります。
今回はエラーとデバッグに関する演習なので、意図的にいくつか誤りを埋め込み、エラーが発生するようにしたプログラムを題材とします。
そして、そのエラーを実際に取り除いていくという演習を行います。
以下はいくつか誤りを埋め込んだプログラムです。
int[] numbers = new int[] { 1, 2, 3, 4, 5 };
Console.WriteLine("<html><body>");
Console.WriteLine("<table border=\"1\">")
int sum = 0;
for (int i = 0; i <= numbers.Lengt; i++)
{
Console.WriteLine($"<tr><td>配列の{i}番目</td><td>{numbers[i]}</td></tr>");
sum = +numbers[i];
}
Console.WriteLine("</table>");
double average = sum / numbers.Length;
Console.WriteLine($"<p>平均値は{average:F2}</p>");
Console.WriteLine("</body></html>");
このプログラムには、3種類のエラー全てを発生させる誤りが含まれています。
- ビルドエラー
- 実行時エラー
- 論理エラー
それでは、これらのエラーの具体例と、その解消方法について見ていきましょう。
ポイント1:ビルドエラー
このプログラムをVisual Studioでビルドしてみましょう。
ビルドが失敗し、画面の下側へエラーの一覧が表示されます。
エラーが2つ出ていますね。
このような、プログラムをビルドするタイミングで検出されるエラーを「ビルドエラー」といいます。
基本的には上から一つずつ見ていき、エラー原因を解消します。
1番目のエラーを見てみましょう。
大事な情報は以下です。
- (1) コード(エラーコード): CS1002
- (2) 説明(エラーメッセージ): ;が必要です
- (3) プロジェクト: ConsoleApp1
- (4) ファイル: Program.cs
- (5) 行: 4
(3)~(5)はエラーの位置です。エラー項目をダブルクリックするとエラーの原因箇所へジャンプすることもできます。
どのようなエラーなのか、まずは、(2)を見ましょう。
「;が必要です」とあります。エラー原因は文法の誤りですね。 セミコロン(;)を追加しましょう。C#の文法では文の最後にセミコロンが必要でした。
再度、ビルドしてみましょう。
エラーが1つ減り、残り1つになりました!
次のエラーについての説明を見ると、以下のようになっています。
- ‘int[]’ に ‘Lengt’ の定義が含まれておらず、型 ‘int[]’ の最初の引数を受け付けるアクセス可能な拡張メソッド ‘Lengt’ が見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足していないことを確認してください
最初の部分(太字にした部分)を見ると、int[]にLengtの定義が含まれていないという問題があると推測できます。
説明を見てもよくわからなかった場合は、エラーコードについても確認してみましょう。以下のように、エラーコードのリンクをクリックします。リンク先はこちらになります。
エラーコードについての詳しい説明や例などを見ることができます。
読んでいくと以下の説明があります。(原文は英語です)
- このエラーは、メソッドを呼び出したり、存在しないクラス メンバーにアクセスしようとしたときに発生します。
まだオブジェクト指向やクラスの説明をしていないのでちょっとわかりにくいですが、このエラーの原因は以下のタイポです。
- (正)Length
- (誤)Lengt
整数配列型にLengthというメンバは定義されていますが、Lengtというメンバは定義されていないので、エラーになっていました。
エラーコードの説明ページは英語です。
Chromeの日本語翻訳機能を使えば、日本語に翻訳して確認することもできます。
それでもわからなければ、エラーコードやエラーメッセージでWeb検索を行ってみるのもよいでしょう。
タイポを修正して再度ビルドしてみましょう。そうすると、以下のように、Visual Studioの画面左下に「ビルド正常終了」と表示されます。
これでビルドエラーについては全て解消できました!
ビルドの中で、ソースコードをオブジェクトコード(コンピュータが理解できるコード)へ変換する作業をコンパイルと言います。
コンパイル時のエラーをコンパイルエラーといいます。コンパイルエラーはビルドエラーの一種で、ビルドエラーのうちの大部分を占めます。
今回紹介した2つのエラーもコンパイルエラーです。
ポイント2:実行時エラー ~ブレークポイントデバッグで解決~
ビルドエラーを全て修正したプログラムは以下のようになっています。
int[] numbers = new int[] { 1, 2, 3, 4, 5 };
Console.WriteLine("<html><body>");
Console.WriteLine("<table border=\"1\">");
int sum = 0;
for (int i = 0; i <= numbers.Length; i++)
{
Console.WriteLine($"<tr><td>配列の{i}番目</td><td>{numbers[i]}</td></tr>");
sum = +numbers[i];
}
Console.WriteLine("</table>");
double average = sum / numbers.Length;
Console.WriteLine($"<p>平均値は{average:F2}</p>");
Console.WriteLine("</body></html>");
このプログラムを実行してみましょう。
すると、以下のように「ハンドルされていない例外」というエラーが発生します。
矢印の位置(for文の中)でエラーが発生し、プログラムが停止した状態になっています。
このように、プログラムの実行時にプログラム続行が不可能となるエラーを実行時エラーといいます。
今回のような「ハンドルされていない例外」は代表的な実行時エラーです。プログラマが最も頻繁に目にする実行時エラーかもしれません。
C#では「例外処理」という、プログラム実行に発生する例外的な状況を、うまく処理する仕組みがあります。
しかし、プログラマが例外処理を適切に行わないと、この「ハンドルされていない例外」が発生します。
C#の例外処理については別の機会に詳しく説明したいと思います。
興味のある方は、C#ガイド 例外と例外処理の記事も参考にしてください。
例外が発生したら、「ハンドルされていない例外」のウィンドウに表示されているエラーメッセージを確認してみましょう。
System.IndexOutOfRangeExceptionとなっており、説明として「インデクスが配列の範囲外でした」と書かれています。
英語がわからなければコピー&ペーストしてGoogle翻訳にかけてしまえばOKです。
Visual Studioのデバッガの機能を使い、プログラム内部の状態を確認してみましょう。
画面左下で、「ローカル」というタブを選択すると、プログラム停止位置のスコープで参照可能な変数とその値の一覧が表示されます。
配列アクセスを行っているnumbers[i]における、カウンタ変数iの値を見ると、5になっていますね。
配列の長さは5であり、配列の番号が0から開始されます、
numbers[5]は配列の範囲外となってしまい、この例外が発生しています。
なぜi=5となってしまっているのかの原因を探っていくと、forの条件文である「i <= numbers.Length」で、「<=」としてしまっているのが誤りとわかります。
これだと、i=5までカウントされてしまうのですね。ここは、「i < numbers.Length」と修正すれば大丈夫です。
修正したプログラムを実行すると、実行時エラーは発生しなくなり、ひとまずプログラムが最後まで走り切るかと思います。
このように、エラーの原因箇所を特定して修正する作業をデバッグといいます。
(デバッガはデバッグを支援するためのツールです)
今回、例外発生によりプログラムが停止しましたが、ブレークポイントというものを設定することで、任意の位置でプログラムを停止させることも可能です。
このように、プログラムを停止させて、プログラム内部の状態をデバッガで確認していく方法を「ブレークポイントデバッグ」と呼びます。
Visual Studioのデバッガを使いこなすことは、デバッグを効率化する上でとても大事です。
デバッガの基本については、Visual Studioデバッガの記事も参考にしてください。
ポイント3:論理エラー ~仮説検証、printfデバッグで解決~
プログラムがやっと実行して走り切るようになったので、実行結果を確認してみましょう。
ビルドエラー、実行時エラーを修正したソースコードは以下のようになっています。
int[] numbers = new int[] { 1, 2, 3, 4, 5 };
Console.WriteLine("<html><body>");
Console.WriteLine("<table border=\"1\">");
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
{
Console.WriteLine($"<tr><td>配列の{i}番目</td><td>{numbers[i]}</td></tr>");
sum = +numbers[i];
}
Console.WriteLine("</table>");
double average = sum / numbers.Length;
Console.WriteLine($"<p>平均値は{average:F2}</p>");
Console.WriteLine("</body></html>");
このプログラムを実行すると、次のような結果になります。
<html><body>
<table border=”1″>
<tr><td>配列の0番目</td><td>1</td></tr>
<tr><td>配列の1番目</td><td>2</td></tr>
<tr><td>配列の2番目</td><td>3</td></tr>
<tr><td>配列の3番目</td><td>4</td></tr>
<tr><td>配列の4番目</td><td>5</td></tr>
</table>
<p>平均値は1.00</p>
</body></html>
実行結果をよく見ると、平均値が1.00となっていておかしいですね。1,2,3,4,5という5つの値の平均ですから、結果は3.00が正しいはずです。
このように、プログラムが動いてはいるものの、ソフトウェアの仕様やプログラマの意図と異なる実行結果となるようなエラーを「論理エラー」といいます。
この平均値を出力するまでにはいくつかの計算を行っており、そのどこかに誤りがあるはずです。
誤りの箇所を絞り込むため、どこから計算が誤ってしまったのか、いくつか仮説をたててみましょう。
これらの仮説を1つずつ検証していき、プログラムの誤り箇所を絞り込んでいきます。
仮説検証の方法としては、ポイント2で紹介したブレークポイントデバッグを使う方法の他に、printfデバッグという方法があります。
printfデバッグとはプログラム実行中に、関心のある変数の値をコンソール画面へ出力し、その情報を見ながらバグ原因箇所を絞り込んでいく方法です。
printfデバッグの名前は、C言語においてコンソール出力を行う命令名がprintfだったことに由来します。
C#で言い換えるならば、「Console.WriteLineデバッグ」といったところでしょうか。
printfデバッグで仮説検証を行ってみましょう。
以下の(a),(b)にそれぞれ、sumの値、averageの値をコンソールへ出力するためのコードを追加しました。
int[] numbers = new int[] { 1, 2, 3, 4, 5 };
Console.WriteLine("<html><body>");
Console.WriteLine("<table border=\"1\">");
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
{
Console.WriteLine($"<tr><td>配列の{i}番目</td><td>{numbers[i]}</td></tr>");
sum = +numbers[i];
Console.WriteLine($"debug: sum = {sum}");//★加算されるたびにsumの値を確認
}
Console.WriteLine("</table>");
double average = sum / numbers.Length;
Console.WriteLine($"debug: average = {average}");//★averageの値を確認
Console.WriteLine($"<p>平均値は{average:F2}</p>");
Console.WriteLine("</body></html>");
このコードを実行すると、実行結果は以下のようになります。
<html><body>
<table border=”1″>
<tr><td>配列の0番目</td><td>1</td></tr>
debug: sum = 1
<tr><td>配列の1番目</td><td>2</td></tr>
debug: sum = 2
<tr><td>配列の2番目</td><td>3</td></tr>
debug: sum = 3
<tr><td>配列の3番目</td><td>4</td></tr>
debug: sum = 4
<tr><td>配列の4番目</td><td>5</td></tr>
debug: sum = 5
</table>
debug: average = 1
<p>平均値は1.00</p>
</body></html>
実行結果を見ると、sumの値が合計値になり、1,3,6…となっていくはずがそうなっていませんね。
「仮説1: sumに値が正しく加算されていない?」が、当たりだったようです。
プログラムをよく見てみると、「sum = +numbers[i];」ではsumへの加算ができていませんね。正しくは、「sum += numbers[i];」という加算の複合代入文ですね。(間違えて、+と=の順番を逆にしていたのですね。)
早速修正をして、再度実行してみましょう。すると実行結果は次のようになります。
<html><body>
<table border=”1″>
<tr><td>配列の0番目</td><td>1</td></tr>
debug: sum = 1
<tr><td>配列の1番目</td><td>2</td></tr>
debug: sum = 3
<tr><td>配列の2番目</td><td>3</td></tr>
debug: sum = 6
<tr><td>配列の3番目</td><td>4</td></tr>
debug: sum = 10
<tr><td>配列の4番目</td><td>5</td></tr>
debug: sum = 15
</table>
debug: average = 3
<p>平均値は3.00</p>
</body></html>
今度は正しく、平均値が3.00と表示されています。
これで、論理エラーも取り除くことができました! めでたし、めでたし。
…としたいのですが、実は、このプログラムにはもう1つの論理エラーがあります。
最初の配列データを{1,2,3,4,5}から{1,2}に変えてみます。平均値は1.50になるはずですね。ところが実行してみると…
<html><body>
<table border=”1″>
<tr><td>配列の0番目</td><td>1</td></tr>
debug: sum = 1
<tr><td>配列の1番目</td><td>2</td></tr>
debug: sum = 3
</table>
debug: average = 1
<p>平均値は1.00</p>
</body></html>
なんと、平均値が1.50ではなく1.00になっています!どこにプログラムの誤りがあるのか、再び仮説検証をしていきましょう。
「debug: average = 1」となっているので、「仮説3: 平均値が正しくない?」が当たりですね。「仮説4: 書式指定を間違えた?」というわけではなさそうです。
平均値の計算が怪しそうだということで、プログラムを確認してみます。
「sum / numbers.Length;」が、整数型/整数型の除算になっており、3/2=1と計算されてしまっていることがわかりました。
「(double)sum / numbers.Length;」が正しいですね。こうすれば、3.0/2=1.5という計算結果になるはずです。
修正したプログラムを実行すると、以下のように正しく計算され、その結果が表示されます。
…
debug: average = 1.5
<p>平均値は1.50</p>
…
今度こそ、論理エラーを全て除くことができました!
デバッグにおける仮説検証の考え方がなんとなくわかってきたでしょうか。
ここでもう1点伝えておきたい大事なことがあります。それは、プログラムの誤りをきちんと全て取り除くためには、様々な入力パターンを試してみる必要があるということです。
{1,2,3,4,5}という配列データの場合には、誤ったコードでも平均値が3.00と正しい値になってしまっていたため、誤りに気づきませんでした。
そして、{1,2}という配列データでプログラムを動かしてみることで、除算方法の誤りに起因する論理エラーを発見することができました。
このように、様々な入力パターンで作成したプログラムがきちんと意図通り(仕様通り)の動作することを確認する作業を「ソフトウェアテスト」といいます。
ソフトウェアテストによってプログラムの意図しない動作(実行時エラー、論理エラー)を発見し、デバッグを行ってその原因箇所の特定、修正を行うということですね。
講義1:エラーとその難易度
演習を通して以下の3つのエラーがあることを学びました。
- (1)ビルドエラー
- (2)実行時エラー
- (3)論理エラー
(1)はソースコードを機械が実行可能なコードへ翻訳するときに発生するエラー、(2),(3)はプログラムを実行しているときに発生するエラーでした。
さて、これらのエラーのやっかいさについて少し考えてみましょう。
エラーを取り除くための難易度については、感覚的には、ざっくり以下のようになるかと思います。
ビルドエラー <<<<<< 実行時エラー・論理エラー
ビルドエラーに比べると、実行時エラー・論理エラーの方がずっとやっかいなのです。
ビルドエラー
最初にプログラミングを始めると、ビルドエラーでつまづくことが多いかもしれません。
しかし、ビルドエラーは慣れてくれば解決することは比較的容易です。理由は以下です。
ビルドエラーを取り除くが容易な理由
- ビルド時に全てのエラーをプログラム実行前に教えてもらえる
- エラーの原因箇所がわかっている
- エラーメッセージを読むと修正方法がわかる事が多い
実行時エラー・論理エラー
これに対して、プログラムを実行してから発生する実行時エラー・論理エラーは次の理由から、ビルドエラーよりも取り除くのは難しいです。
実行時エラー・論理エラーを取り除くのが難しい理由
- エラーがどれだけ発生するかは事前にわからない
- エラーの原因箇所を特定することが難しい
演習でも説明しましたが、ビルドエラーと異なり、実行時エラー・論理エラーがいくつあるかは誰にもわかりません。
これについては、様々な入力パターンを試すなど、テストをきちんと行うことが重要です。
加えて、エラーが発生したとわかったとしても、その原因となっている箇所を特定するのは大変です。
演習では10行ちょっとのソースコードでしたが、数千・数万行のソースコードとなると、原因箇所の特定がいかに難しいかは想像できるかと思います。
実行時エラーと論理エラーを比較すると、論理エラーの方がよりやっかいなエラーです。
実行時エラーは発生するとプログラムが停止するため、エラーの発生がすぐにわかりますが、論理エラーは見た目上、プログラムは動き続けているため、エラーが発生したとすぐにはわからないためです。
論理エラーを発見するためには、プログラムの出力が仕様通りのものになっているかを丁寧に確認していく必要があります。
型を指定することで型の整合性をチェックすることが可能となり、プログラム実行前にビルドエラーとして多くのエラーを事前に検出できるようになっているのです。
C#を含めこのように型を指定してコードを記述し、ビルド時に型チェックを行う言語を「静的型付き言語」といいます。
静的型付き言語では、プログラムの誤りをビルドエラーとしてなるべくプログラム実行前に検出し、プログラム実行時に発生するやっかいなエラーを減らしています。
講義2:デバッグ方法の特徴
実行時エラー・論理エラーの原因箇所特定を効率よく行うには、仮説検証の考え方と、2つのデバッグ方法(printfデバッグ、ブレークポイントデバッグ)をうまく使っていくことが重要です。
printfデバッグ、ブレークポイントデバッグはそれぞれメリット・デメリットがあるため、うまく使い分けるとよいでしょう。
printfデバッグ
printfデバッグのメリット、デメリットは以下のようになります。
- ◯ 変数値の時系列変化などを一覧化して確認できる
- ◯ 統合開発環境(デバッガ)がなくても使える
- ✕ あらかじめ決めた変数値しか確認できない
- ✕ デバッグ用のコードが埋め込まれるのでソースコードは汚くなる
ブレークポイントデバッグ
ブレークポイントデバッグのメリット、デメリットは以下のようになります。
- ◯ プログラム停止時点でのあらゆる変数値を確認できる
- ✕ 停止時点の情報しか確認できない(過去の情報を含めて一覧化して確認することができない)
これらのデバッグ方法はもちろん併用することも可能です。
うまく使いこなして、仮説検証を効率よく行っていきましょう。
まとめ
今回は、ビルドエラー、実行時エラー、論理エラーの3種類があることを説明しました。
そして、デバッグにおける仮説検証の考え方、printfデバッグ、ブレークポイントデバッグという代表的なデバッグ方法についても学びました。
これで、プログラムを書いていてエラーに遭遇したときに、どのようなエラーであるのかがわかるようになりました。
どのような考え方でそのエラーへ対処していけばよいかの基本もわかったかと思います。
次回からはいよいよオブジェクト指向の説明に入っていきます。