例外(exception)とは、 本来ならばプログラム中で起こってはいけないことが起こってしまうことをいいます。 堅牢なプログラムを作成するためには、 例外が起こったときでもプログラムが異常な動作をしないよう、 しっかりと例外処理(exception handling)を行う必要があります。
C# では、例外処理を行うための専用の構文が用意されていて、 プログラマが例外処理を容易に行えるようになっています。
例外の例を挙げると、ユーザーが想定外の文字列を入力してきたときや、 プログラムに必要なファイルが開けなかったときなどがあります。
例えば、文字列を整数に変換することを考えてみます。 簡単化のため、とりあえず正の整数のみを扱うことにします。 想定外の文字列が来ないものと仮定するとプログラムは以下のようになります。
// 文字→整数 static int CharToInt(char c) { return c - '0'; } // 文字列→整数 static int StringToInt(string str) { int val = 0; foreach(char c in str) { int i = CharToInt(c); val = val * 10 + i; } return val; }
当然、この関数に対して想定外の文字列を入力すると、おかしな結果が得られます。
static void Main() { Console.Write("{0}\n", StringToInt("12345")); Console.Write("{0}\n", StringToInt("12a45")); // 途中に数字以外の文字が }
12345
16945 ←変な値が出力されてる
利用側が「想定外の文字列は絶対に入力しない」とか、 「変な結果が出てきても文句は言わない」とかいう風に開き直っているのなら、 別にこれでも問題はありません。 しかし、通常は想定外の文字列が入力されていないかどうか調べる手段が必要になると思います。
このような例外が起きたとき、 例外処理用の構文が用意されていない言語ではどのように対処していたかというと、 「通常はありえない値を返す」とか、 「関数の戻り値とは別に、関数が正しく終了したかどうかを示すフラグを返す」 といった手段を用いていました。
例えば、今回の場合、正の整数しか想定していないので、 想定外の文字列が来たときには負の数を返すことにしておけば、 例外が起きたかどうか調べることが出来ます。
// 文字→整数 static int CharToInt(char c) { if('0' <= c && c <= '9') return c - '0'; else return -1; // 想定外の文字が入力された場合、-1 を返す。 } // 文字列→整数 static int StringToInt(string str) { int val = 0; foreach(char c in str) { int i = CharToInt(c); if(i == -1) return -1; // 想定外の文字列が入力された場合、-1 を返す。 val = val * 10 + i; } return val; }
関数の利用側のコードは以下のようになります。
static void Main() { int i; i = StringToInt("12345"); if(i == -1) Console.Write("想定外の文字列が入力されました"); else Console.Write("{0}\n", i); i = StringToInt("12a45"); if(i == -1) Console.Write("想定外の文字列が入力されました"); else Console.Write("{0}\n", i); }
12345 想定外の文字列が入力されました
上述したように、例外処理専用の構文を用いなくても例外処理を行えます。 しかし、上述したような方法にはいくつか欠点があります。 その欠点を以下に挙げます。
例外処理構文を用いるとこれらの問題を解決することが出来ます。 ここでようやく本題に入るわけですが、 それでは、C# の例外処理構文について説明していきたいと思います。
まず、関数定義側、すなわち、例外が発生する可能性のある側では、 throw 文を使って例外が起こったことを利用側に知らせます。 throw 文は以下のようにして使用します。
throw 例外クラスのインスタンス
この throw 文は想定外のことが起こった場所に挿入します。 このような処理を「例外を投げる」といいます。 例外が投げられると、正常動作部の処理は中断され、例外処理部が呼び出されます。
throw 文によって投げられる例外は、
System.Exception クラスの派生クラスのインスタンスです。
それ以外のクラスのインスタンスを throw することは出来ません。
例えば、throw new Exception(); というようにします。
例として先ほどの文字列→整数変換関数を throw 文を使って書き直してみましょう。
// 文字→整数 static int CharToInt(char c) { if(c < '0' || '9' < c) throw new FormatException(); // 不正な文字が入力された場合、例外を投げる return c - '0'; } // 文字列→整数 static int StringToInt(string str) { int val = 0; foreach(char c in str) { int i = CharToInt(c); val = val * 10 + i; } return val; }
次に、関数利用側、すなわち、例外を処理する側では、 try-catch-finally 文を使って例外を処理します。 try-catch-finally 文は以下のようにして使用します。
try { 例外が投げられる可能性のあるコード } catch(例外の種類) { 例外処理コード } finally { 例外発生の有無にかかわらず実行したいコード リソースの破棄などを行う }
こちらも例として、先ほどの文字列→整数変換関数利用側コードを try-catch 文を使って書き直してみましょう。 (finally については 「リソースの破棄」 で例を示します。)
static void Main() { try { Console.Write("{0}\n", StringToInt("12345")); Console.Write("{0}\n", StringToInt("12a45")); //↑ ここで FormatException 例外が投げられる。 } catch(FormatException) { Console.Write("想定外の文字列が入力されました"); } }
try-catch 文を使った例外処理には以下のような利点があります。
.NET Frameowrk が標準で提供する例外クラスのうち、よく出てくるもの/よく使うものをいくつか例に挙げます。
| クラス名 | throw される状況 |
|---|---|
| System 名前空間 | |
| ArgumentException | メソッドの引数が変な場合。ArgumentNullExceptionやArgumentOutOfRangeException以外の場合で変な時に使う。 |
| ArgumentNullException | 引数がnullの場合。 |
| ArgumentOutOfRangeException | メソッドの許容範囲外の値が引数として渡された場合。 |
| ArithmeticException | 算術演算によるエラーの基本クラス。OverflowException, DivideByZeroException, NotFiniteNumberException以外の算術エラーを示したければ使う。 |
| OverflowException | 算術演算やキャストでオーバーフローが起きた場合。 |
| DivideByZeroException | 0で割ったときのエラー。 |
| NotFiniteNumberException | 浮動小数点値が無限大の場合。 |
| FormatException | 引数の書式が仕様に一致していない場合。 |
| IndexOutOfRangeException | 配列のインデックスが変な場合。 |
| InvalidCastException | 無効なキャストの場合。 |
| InvalidOperationException | 引数以外の原因でエラーが起きた場合。 |
| ObjectDisposedException | Dispose済みのオブジェクトで操作が実行される場合。 |
| NotImplementedException | メソッドが未実装の場合。 |
| NotSupportedException | 呼び出されたメソッドがサポートされていない場合、または呼び出された機能を備えていないストリームに対して読み取り、シーク、書き込みが試行された場合。 |
| NullReferenceException | nullオブジェクト参照を逆参照しようとした場合。 |
| PlatformNotSupportException | 特定のプラットフォームで機能が実行されない場合。 |
| TimeoutException | 指定したタイムアウト時間が経過した場合。 |
| System.Collections.Generics 名前空間 | |
| KeyNotFoundException | コレクションに該当するキーが無い場合。 |
| System.IO 名前空間 | |
| DirectoryNotFoundException | ディレクトリが無い場合。 |
| FileNotFoundException | ファイルが無い場合。 |
| EndOfStreamException | ストリームの末尾を超えて読み込もうとしている場合。 |
一般に、tyr-catch を用いた例外処理は、 if 文などを使った値のチェックに比べて、 実行速度が遅いといわれています。 try ブロックで囲んだだけでは(例外が発生しなければ)ほとんどオーバーヘッドはないのですが、 例外発生時には少し大き目のコストが発生します。
このコストを考えても、try-catch を用いるメリットの方が大きいのですが、 まあ、避けれるのならばコストの大きな処理は避けたいというのがプログラマの心情というものです。
ということで、例外の使い方(特に、避けれる例外を避ける方法)について別ページで説明をします → 「[雑記] 例外の使い方」 。