なぜ、例外の理解が重要なのか、その理由
この記事では、当社の新人エンジニア研修の参考に C# を解説します。
前回は、「カプセル化と情報隠蔽で部品の完成度を高める」について学びました。
今回は例外処理(Exception Handling)について解説します。例外は継承の仕組みを使っており、エラー処理を効率よく行うために重要です。
1. 例外(Exception)とは?
例外とは、プログラムが正常に続行できない状況が起こったときに発生するエラー通知です。たとえば「0で割った」「範囲外の配列アクセス」「null参照」などが挙げられます。これらのエラーを「例外オブジェクト」として発生させ、必要に応じてキャッチ(補足)して処理を行います。
例外の典型的な使い方は「アプリが落ちても、データはちゃんと保存してから終了」や「ユーザーにエラーメッセージを表示」などです。もし例外処理がなければ、エラーが起きた瞬間にアプリが強制終了してしまい、ユーザーデータも残らないなど悲惨なことになるかもしれません。
C#で典型的な例外には以下のようなものがあります:
DivideByZeroException
IndexOutOfRangeException
NullReferenceException
InvalidCastException
FormatException
いずれも実行時に起こります。
2. try
~ catch
~ finally
以下のような構文で例外を捕捉できます。
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
に書いた処理は例外の発生有無にかかわらず必ず実行されます。リソース解放や必須の後処理を置くのに便利です。
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
ステートメント
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()
が呼ばれます。
5. 例外クラスの体系
C#では「すべての例外は Exception
の派生で、catchは任意」です。
try-catch
は強制されません。try-catch
を使うタイミングは「必要に応じて」設計者が判断する文化です
System.Exception
クラス
すべての例外は System.Exception
を継承しており、そこから派生した System.SystemException
があります。具体的には System.DivideByZeroException
や System.FormatException
などが存在します。
System.Object
└─ System.Exception
└─ System.SystemException
├─ DivideByZeroException (ゼロによる除算時の例外)
├─ IndexOutOfRangeException (配列などで範囲外を参照した場合の例外)
├─ NullReferenceException (null参照を使用した際の例外)
├─ InvalidCastException (無効な型変換を行った場合の例外)
└─ 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#では例外が起きたら勝手に上位メソッドへ伝播します。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() ... |
まとめができたら、アウトプットとして演習問題にチャレンジしましょう。
以上、今回は「例外処理で想定外の事態に強いシステムにする」方法について見てきました。
例外処理を使うことでプログラムはトラブルの発生をユーザーに伝えることができ、途中終了を避けることができるのでした。また、継承の仕組みを上手く応用しているのも興味深いところでしたね。
実際にお客様の依頼で開発するアプリでは、詳細なエラー情報は画面に表示しないことが多いのです。特に深刻なエラーが起こった場合はなおさらです。
知識が不十分なお客様をいたずらに不安にさせることになりますし、下手をすれば責任問題に発展し、会社同士のもめ事にもなりかねないからです。
次回は、「13章. Listでもっと配列を便利に使いこなす」です。