++C++; // 未確認飛行 C MVVMパターンを使ったクロス・ターゲット開発 C#たんと学ぶ わりと硬派なソフトウェア開発講座 番外編「Windows Phone 7」

Top総合 目次C# によるプログラミング入門

例外処理

このエントリーをはてなブックマークに追加

目次

キーワード

概要

例外(exception)とは、 本来ならばプログラム中で起こってはいけないことが起こってしまうことをいいます。 堅牢なプログラムを作成するためには、 例外が起こったときでもプログラムが異常な動作をしないよう、 しっかりと例外処理(exception handling)を行う必要があります。

C# では、例外処理を行うための専用の構文が用意されていて、 プログラマが例外処理を容易に行えるようになっています。

ポイント
  • 例外: 「開こうとしたファイルが存在しなかった」など、特別な対処が必要な状況。
  • 例外への対処には、例外用の構文があるのでそれを使いましょう。
  • try { 例外が発生する可能性のあるコード } catch(例外) { 例外処理 }

例外処理とは

例外の例を挙げると、ユーザーが想定外の文字列を入力してきたときや、 プログラムに必要なファイルが開けなかったときなどがあります。

例えば、文字列を整数に変換することを考えてみます。 簡単化のため、とりあえず正の整数のみを扱うことにします。 想定外の文字列が来ないものと仮定するとプログラムは以下のようになります。

// 文字→整数
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 文は想定外のことが起こった場所に挿入します。 このような処理を「例外を投げる」といいます。 例外が投げられると、正常動作部の処理は中断され、例外処理部が呼び出されます。

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
{
  例外発生の有無にかかわらず実行したいコード
  リソースの破棄などを行う
}

こちらも例として、先ほどの文字列→整数変換関数利用側コードを 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 文の利点

try-catch 文を使った例外処理には以下のような利点があります。

  • 正常動作部と例外処理部の区別が明確になります。
    • try の中には動作が正常な時の処理が、 catch の中には例外発生時の対処のみが書かれます。
  • 関数利用側に例外処理させることを強制できます。
    • もし、正しく例外処理しなければ(throw された例外を catch しなければ)、プログラムは強勢終了されます。
    • 対処のしようのないエラーがあった場合、対処できないままプログラムが動き続けるよりは、強制終了される方が後々困ることが少なくなります。
  • 例外処理部(catch 節)や、正常・例外問わず必ず実行する必要なある部分(finaly 節)が一か所にまとまります。
    • 同じような処理を何か所も書く必要がなくなります。

標準で用意されている例外クラス

.NET Frameowrk が標準で提供する例外クラスのうち、よく出てくるもの/よく使うものをいくつか例に挙げます。

クラス名throw される状況
System 名前空間
ArgumentExceptionメソッドの引数が変な場合。ArgumentNullExceptionやArgumentOutOfRangeException以外の場合で変な時に使う。
ArgumentNullException引数がnullの場合。
ArgumentOutOfRangeExceptionメソッドの許容範囲外の値が引数として渡された場合。
ArithmeticException算術演算によるエラーの基本クラス。OverflowException, DivideByZeroException, NotFiniteNumberException以外の算術エラーを示したければ使う。
OverflowException算術演算やキャストでオーバーフローが起きた場合。
DivideByZeroException0で割ったときのエラー。
NotFiniteNumberException浮動小数点値が無限大の場合。
FormatException引数の書式が仕様に一致していない場合。
IndexOutOfRangeException配列のインデックスが変な場合。
InvalidCastException無効なキャストの場合。
InvalidOperationException引数以外の原因でエラーが起きた場合。
ObjectDisposedExceptionDispose済みのオブジェクトで操作が実行される場合。
NotImplementedExceptionメソッドが未実装の場合。
NotSupportedException呼び出されたメソッドがサポートされていない場合、または呼び出された機能を備えていないストリームに対して読み取り、シーク、書き込みが試行された場合。
NullReferenceExceptionnullオブジェクト参照を逆参照しようとした場合。
PlatformNotSupportException特定のプラットフォームで機能が実行されない場合。
TimeoutException指定したタイムアウト時間が経過した場合。
System.Collections.Generics 名前空間
KeyNotFoundExceptionコレクションに該当するキーが無い場合。
System.IO 名前空間
DirectoryNotFoundExceptionディレクトリが無い場合。
FileNotFoundExceptionファイルが無い場合。
EndOfStreamExceptionストリームの末尾を超えて読み込もうとしている場合。

例外処理の指針

一般に、tyr-catch を用いた例外処理は、 if 文などを使った値のチェックに比べて、 実行速度が遅いといわれています。 try ブロックで囲んだだけでは(例外が発生しなければ)ほとんどオーバーヘッドはないのですが、 例外発生時には少し大き目のコストが発生します。

このコストを考えても、try-catch を用いるメリットの方が大きいのですが、 まあ、避けれるのならばコストの大きな処理は避けたいというのがプログラマの心情というものです。

ということで、例外の使い方(特に、避けれる例外を避ける方法)について別ページで説明をします → 「[雑記] 例外の使い方」 。

[お問い合わせ](q)