なぜ、例外の理解が重要なのか、その理由
この記事では、当社の新人エンジニア研修の参考に C# を解説します。
前回はカプセル化について学びました。
今回は例外処理(Exception Handling)について解説します。例外は継承の仕組みを使っており、エラー処理を効率よく行うために重要です。
1. 例外(Exception)とは?
例外とは、プログラムが正常に続行できない状況が起こったときに発生するエラー通知です。たとえば「0で割った」「範囲外の配列アクセス」「null参照」などが挙げられます。C#でもこれらのエラーを「例外オブジェクト」として発生させ、必要に応じてキャッチ(補足)して処理を行います。
例外の典型的な使い方は「アプリが落ちても、データはちゃんと保存してから終了」や「ユーザーにエラーメッセージを表示」などです。もし例外処理がなければ、エラーが起きた瞬間にアプリが強制終了してしまい、ユーザーデータも残らないなど悲惨なことになるかもしれません。
C#で典型的な例外には以下のようなものがあります:
DivideByZeroException
()IndexOutOfRangeException
()NullReferenceException
()InvalidCastException
()FormatException
()
いずれも実行時に起こります。
2. try
~ catch
~ finally
C#では、Java同様に以下のような構文で例外を捕捉できます。
try
{
// 例外が発生する可能性のある処理
}
catch (例外の型1 変数名1)
{
// 例外の型1を捕捉した場合の処理
}
catch (例外の型2 変数名2)
{
// 例外の型2を捕捉した場合の処理
}
finally
{
// 例外の有無に関わらず必ず実行される処理
}
単純例
using System;
public class Example01
{
public static void Main()
{
try
{
Console.WriteLine(5 / 0);
}
catch (DivideByZeroException e)
{
Console.WriteLine("例外が発生しました");
Console.WriteLine(e);
}
Console.WriteLine("このプログラムを終了します");
}
}
<実行結果(例)>
例外が発生しました
System.DivideByZeroException: Attempted to divide by zero.
このプログラムを終了します
try
ブロック内で5/0
が実行され、DivideByZeroException
が投げられるcatch (DivideByZeroException e)
で捕捉し、「例外が発生しました」と表示。その後プログラムは正常に進行し、最後の行が実行されます。
finally
ブロック
finally
に書いた処理は例外の発生有無にかかわらず必ず実行されます。リソース解放や必須の後処理を置くのに便利です。Javaの説明と同様、ファイルやDBを閉じるなどの用途で使われます。
using System;
public class Example02
{
public static void Main()
{
try
{
Console.WriteLine(int.Parse("NotAnInt"));
}
catch (FormatException e)
{
Console.WriteLine(e);
}
finally
{
Console.WriteLine("例外発生の有無に関わらず必ず実行したい処理です");
}
Console.WriteLine("このプログラムを終了します");
}
}
<実行結果>
System.FormatException: Input string was not in a correct format.
例外発生の有無に関わらず必ず実行したい処理です
このプログラムを終了します
finally
に書いた行はエラーが起きても必ず実行。
3. 複数例外を順にキャッチ
複数の異なる例外が起こり得る場合、それぞれに対応したcatch
節を並べます。
using System;
public class Example03
{
public static void Main()
{
try
{
int[] numbers = { 1, 2, 3 };
numbers[3] = 4; // IndexOutOfRangeException
Console.WriteLine(5 / 0); // DivideByZeroException
Console.WriteLine(int.Parse("NotAnInt")); // FormatException
}
catch (IndexOutOfRangeException e)
{
Console.WriteLine($"Index問題: {e}");
}
catch (DivideByZeroException e)
{
Console.WriteLine($"割り算問題: {e}");
}
catch (FormatException e)
{
Console.WriteLine($"変換問題: {e}");
}
Console.WriteLine("このプログラムを終了します");
}
}
最初に IndexOutOfRangeException
が起きると、そこですぐ catch(IndexOutOfRangeException ...)
にジャンプし、以降の行は実行されません。
4. using
ステートメント
C#のusingステートメントはIDisposableインターフェースを実装しているオブジェクトを自動的にDispose()で解放します。
using System;
using System.IO;
public class Example04
{
public static void Main()
{
Console.Write("ファイル名を入力してください: ");
string filePath = Console.ReadLine();
try
{
using (StreamReader sr = new StreamReader(filePath))
{
string content = sr.ReadToEnd();
Console.WriteLine(content);
} // ここで自動的に sr.Dispose() が呼ばれる
}
catch (FileNotFoundException e)
{
Console.WriteLine($"ファイルが見つかりません: {e.FileName}");
}
catch (IOException e)
{
Console.WriteLine($"IOエラー: {e.Message}");
}
}
}
using(...)
ブロックを抜けると自動で Dispose()
(Javaでいう close()
)が呼ばれます。
これがJavaのtry-with-resourcesに相当する記述方法です。
5. 例外クラスの体系
Java同様に、C#にも例外クラスの階層がありますが、C#にはチェック例外という概念がありません。
Javaのように「Exception
を継承しているがRuntimeException
を継承していない場合はチェック例外」という区別がないため、すべての例外は非チェック例外となります。
C#では「throwできるのは全部Exception
派生クラス」というルールになっていて、メソッド宣言でthrows
的なものを書く必要はありません。
System.Exception
クラス
すべての例外は System.Exception
を継承しており、そこから派生した System.SystemException
や System.ApplicationException
などがあります。そして具体的には System.DivideByZeroException
や System.FormatException
などが存在します。
throw
キーワード
C#では、throw
を使って例外を発生させることができます。
public static int ParsePositiveInt(string s)
{
int num = int.Parse(s);
if (num < 0)
{
throw new ArgumentOutOfRangeException(nameof(s), "正の数を指定してください");
}
return num;
}
6. 例外を呼び出し元に投げる
C#では「throws」に当たる仕組みはないので、例外が起きたら勝手に上位メソッドへ伝播します。throw
を使うと「現在のスコープを抜けて、呼び出し元のメソッドへ例外を送る」動きになります。
もしキャッチしなければ更に呼び出し元へ伝播し、最終的にはランタイムが受け取ってアプリケーションが終了し、スタックトレースが表示されます。
public class Sample
{
public Sample()
{
throw new Exception("Custom error in constructor");
}
}
public class Example10
{
public static void Main()
{
Sample s = new Sample(); // => ここで Exceptionが投げられ、キャッチしないとプログラム終了
}
}
<実行結果(例)>
Unhandled exception. System.Exception: Custom error in constructor
at Sample..ctor() ...
at Example10.Main() ...
スタックトレースを見れば、どこで例外が発生したかが分かります。
7. 例外クラスを自作する
C#ではオリジナル例外を作れます。Exception
クラスを継承して用途に合ったクラス名・追加情報を持たせることがあります。
public class MyCustomException : Exception
{
public MyCustomException(string message) : base(message)
{
}
}
public class Example11
{
public static void Main()
{
throw new MyCustomException("この例外はユーザー定義です");
}
}
<実行結果>
Unhandled exception. MyCustomException: この例外はユーザー定義です
at Example11.Main() ...
まとめができたら、アウトプットとして演習問題にチャレンジしましょう。
以上、今回は「例外処理で想定外の事態に強いシステムにする」方法について見てきました。
例外処理を使うことでプログラムはトラブルの発生をユーザーに伝えることができ、途中終了を避けることができるのでした。また、継承の仕組みを上手く応用しているのも興味深いところでしたね。
実際にお客様の依頼で開発するアプリでは、詳細なエラー情報は画面に表示しないことが多いのです。特に深刻なエラーが起こった場合はなおさらです。
知識が不十分なお客様をいたずらに不安にさせることになりますし、下手をすれば責任問題に発展し、会社同士のもめ事にもなりかねないからです。
次回は、「」です。
ということで、次回を楽しみに待っていてください。