例外(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; }
当然、この関数に対して想定外の文字列を入力すると、おかしな結果が得られます。
public 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; }
関数の利用側のコードは以下のようになります。
public 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 については 「リソースの破棄」 で例を示します。)
public static void Main() { try { Console.Write("{0}\n", StringToInt("12345")); Console.Write("{0}\n", StringToInt("12a45")); //↑ ここで FormatException 例外が投げられる。 } catch(FormatException) { Console.Write("想定外の文字列が入力されました"); } }
一般に、tyr-catch を用いた例外処理は、 if 文などを使った値のチェックに比べて、 実行速度が遅いといわれています。 try ブロックで囲むだけで、例外が発生しなければ、あまり大きなオーバーヘッドはないのですが、 例外発生時には少し大き目のコストが発生します。
このコストを考えても、try-catch を用いるメリットの方が大きいのですが、 まあ、避けれるのならばコストの大きな処理は避けたいというのがプログラマの心情というものです。 では、どういうときには try-catch を用いるべきで、 どういうときには避けてもいいのでしょうか。
その1つの指針として、 ユーザからの入力や、ファイルの状態など、 本当にその時々によって変化し、(コンパイル時には)全く予測の付かないものの処理に try-catch を使うというのがあります。 逆に言うと、例外が起こる場所・タイミングが完全に分かるようなものには try-catch 構文は使いません。 このような予測の付く例外には if 文などの条件分岐構文で対応します。