前回は、nullとnull許容型の基本について解説しました。
今回は、C#の例外処理(try-catch)の基本と、実務でのポイントについて丁寧に解説します。
例外処理(try-catch)とは?
プログラムを作っていると、どうしても予想しきれないエラー(=例外)が発生することがあります。
こうした「予期せぬエラー」に何も対策をしないと、プログラムが突然止まってしまったり、ユーザーに不親切な結果になってしまいます。
そこで役立つのが、C#で用意されている「try-catch」という例外処理の仕組みです。
- 「try」はエラーが発生する可能性があるコードを記述する場所です。
- 「catch」はtryの中でエラーが起きたら行う処理を記述する場所です。
try-catchを使えば、エラーが発生してもプログラムを安全に制御できるようになります。
実際のコード例を見ながらtry-catchの基本的な使い方を確認してみましょう。
サンプルコード
以下はゼロで割るエラーが発生するコードの例です。
割り算で「割る数が0」の場合は数学的に答えが存在しないため、C#をはじめ多くのプログラミング言語ではエラー(例外)が発生します。
var a = 10;
var b = 0;
try
{
// エラーが起きる可能性がある処理
var result = a / b;
Console.WriteLine("計算結果:" + result);
}
catch (DivideByZeroException ex)
{
// エラーが発生した時の処理
Console.WriteLine("エラー:0で割ることはできません。");
Console.WriteLine("エラー詳細:" + ex.Message);
}
このコードを実行すると、ゼロ除算エラーが起こります。
しかし、try-catchのおかげでプログラムは突然停止せず、代わりにcatchブロックの中で設定した安全な処理が実行されます。具体的には、
エラー:0で割ることはできません。
エラー詳細:Attempted to divide by zero.
というメッセージがユーザーに表示されます。
このように、「try-catch」という仕組みを使うことで、エラーが発生しても安全にプログラムを動かし続けることができます。
finallyブロックを使った後処理
実際の開発では「エラーが起きても起きなくても、必ず実行したい処理」が必要になる場面もよくあります。
例えば、開いたファイルやデータベース接続をきちんと閉じてリソースを解放する場合などです。
このようなときに役立つのが、finallyブロックです。
例えば、ファイルを開いて中身を読み込むプログラムの場合、ファイルの有無や読み込み時のエラーに関係なく、最後にファイルを閉じる処理を必ず実行したい場合はfinallyブロックを使います。
StreamReader sr = null;
try
{
// 「input.txt」というファイルを読み込んで中身をコンソールに表示
sr = new StreamReader("input.txt");
Console.WriteLine(sr.ReadToEnd());
}
catch (FileNotFoundException ex)
{
// ファイルが見つからなかった場合はエラーメッセージをコンソールに表示
Console.WriteLine("ファイルが見つかりませんでした:" + ex.Message);
}
finally
{
// エラーが起きても起きなくても最後にファイルを閉じる
if (sr != null)
{
sr.Close();
}
}
この例では、ファイルの読み込みに失敗しても、finallyブロック内のsr.Close()
が必ず実行されるので、リソースの無駄遣いや不具合を防ぐことができます。
using文でさらにシンプルに書く
C#には、リソース管理をより簡単にしてくれる「using文」という仕組みもあります。
using文を使うと、ブロックの終わりで自動的にリソースが解放されるので、finallyブロックを書かなくても済みます。
try
{
using (var sr = new StreamReader("input.txt"))
{
Console.WriteLine(sr.ReadToEnd());
} // ブロックを抜けるとsrが自動的に閉じられる
}
catch (Exception ex)
{
Console.WriteLine("エラー:" + ex.Message);
}
using文は、ファイルだけでなくデータベース接続やネットワークリソースの管理にもよく使われます。
リソースを扱う際は、finallyブロックやusing文を活用して、後片付けを忘れずに行いましょう。
実務でよく使う代表的な例外クラス
C#には、様々なエラーの種類ごとに「例外クラス」が標準で用意されています。
実際の開発でよく登場する代表的な例外クラスを紹介します。
- ArgumentException
メソッドの引数に間違った値が渡された場合に発生します。たとえば、「1以上の数字が必要なのに0が渡された」など、引数の内容が正しくないときによく使われます。 - InvalidOperationException
オブジェクトの状態と操作が合っていない場合に発生します。「まだ初期化していないリストからデータを取り出そうとした」など、本来できない操作をしたときに起こります。 - NullReferenceException
nullの変数やオブジェクトを使おうとしたときに発生します。プログラムを書く上で最も頻繁に遭遇する例外です。 - IOException
ファイルやネットワーク通信などの入出力処理で問題が起きたときに発生します。「ファイルが見つからない」「ファイルが他で使われている」などで発生します。
特に「NullReferenceException」はよく発生するので、事前にnullかどうかを確認する習慣をつけるようにしましょう。
※nullの扱いについては前回の記事「nullとnull許容型の基本」を参照してください。
C#では、標準の例外クラスだけでなく、自分で「カスタム例外」クラスを作ることもできます。初心者のうちはあまり使う場面は少ないので詳しくは触れませんが、業務で特別なルールのエラーを表現したい場合などに使われます。
例外を自分で投げる(throw)
ここまでは、プログラムが勝手に発生させる例外の処理方法について説明してきましたが、開発者自身が「ここはエラーにしたい」と判断したときに、例外を自分で発生させることもできます。
例えば、メソッドの引数に「ありえない値」が渡ってきた場合などに使います。
public void SetAge(int age)
{
if (age < 0)
{
// 年齢がマイナスだった場合は意図的に例外を発生させる
throw new ArgumentOutOfRangeException(nameof(age), "年齢に負の値は指定できません。");
}
// 正しい場合の処理...
}
このようにthrow
を使って例外を発生させることで、「ここで問題が起きている」というサインを他のコードや開発者に伝えることができます。
また、.NET 6以降のC#では、よくある「nullチェック」などの場面で、より簡単に例外を発生させる専用の書き方も用意されています。
// objがnullだった場合、自動的にArgumentNullExceptionを発生させる
ArgumentNullException.ThrowIfNull(obj);
if文によるnull判定が省略できるので、覚えておくと便利です。
アプリケーション全体での例外処理(グローバル例外処理)
ここまで、エラーが起こりそうな場所にtry-catchを書いて安全に処理する方法を説明しました。
しかし、実際のアプリケーションでは「どこにもcatchを書いていなかったエラー」や「予想外のエラー」がどうしても発生することがあります。
このような「キャッチしきれなかった例外」をまとめて安全に処理するために、アプリ全体で例外を管理する仕組み(グローバル例外処理)を用意するのが一般的です。
グローバル例外処理の考え方
グローバル例外処理は、プログラムの一番上の部分で「もしどこかでエラーが起きても、最後にここで必ず受け止める」という役割を持っています。これによって、
- エラーが発生してもプログラム全体が予期せず止まるのを防げる
- エラーの詳細な情報を開発者向けのログとして残せる
- ユーザーには分かりやすくて安全なメッセージだけを表示できる
といったメリットがあります。
この考え方はWebアプリやデスクトップアプリ、バッチ処理など、どんな種類のアプリケーションでも共通して使えます。
Webアプリ(ASP.NET Core)での例
例えば、ASP.NET CoreというWebアプリ開発の仕組みでは、Program.cs
内の「ミドルウェア」と呼ばれる部分でグローバル例外処理を設定できます。
以下はその一例です。
app.UseExceptionHandler(builder =>
{
builder.Run(async context =>
{
var exceptionFeature = context.Features.Get<IExceptionHandlerPathFeature>();
var exception = exceptionFeature?.Error;
// 開発者向けに詳細なエラー情報をログに残す
Console.WriteLine($"未処理の例外が発生しました:{exception?.ToString()}");
// ユーザーには分かりやすいエラーメッセージのみを表示
context.Response.StatusCode = 500;
await context.Response.WriteAsync("サーバーエラーが発生しました。再度お試しください。");
});
});
このようにしておくと、プログラムのどこでエラーが起きても「最後の砦」としてここで安全に処理されます。
エラーの内容をしっかりログに残しつつ、ユーザーには余計な情報を見せずに「再度お試しください」とだけ伝えることができます。
例外処理のベストプラクティス
ここでは、実務の現場における例外処理の基本的なルールを、具体的なサンプルコードを交えて解説します。
例外は具体的にキャッチする
catch
を使う時には、可能な限り「具体的な例外」を指定しましょう。
全ての例外をまとめてキャッチするのではなく、起こりうる例外ごとに分けて処理すると、エラーの原因が特定しやすくなります。
例えば、ファイル操作の例を見てみましょう。
try
{
string text = File.ReadAllText("example.txt");
Console.WriteLine(text);
}
catch (FileNotFoundException ex)
{
// ファイルが無い場合だけの対応
Console.WriteLine("ファイルが見つかりませんでした:" + ex.Message);
}
catch (IOException ex)
{
// それ以外のファイル操作のエラー
Console.WriteLine("ファイルの読み込み中にエラーが発生しました:" + ex.Message);
}
catch (Exception ex)
{
// 上記以外の予期しないエラー
Console.WriteLine("予期せぬエラーが発生しました:" + ex.Message);
}
このように、よく起きる例外やよく調べる例外は個別にcatchして、最後に全体をcatch(Exception)でまとめておくのが基本です。
具体的な例外ごとにメッセージを変えたり、処理を変えたりできるので、エラーの原因が特定しやすくなり、バグの発見や修正もしやすくなります。
例外を「握り潰さない」
初心者が陥りがちな失敗として、エラーをキャッチしたにもかかわらず「何もしない」というケースがあります。
エラーが起きたことを記録したり、ユーザーに伝えたりしないまま無視してしまうことを「例外を握り潰す」と言います。
例外を握り潰してしまうと、「どこで何が起きているのか」分からなくなってしまい、バグの発見や修正がとても難しくなります。
悪い例(握り潰し)
try
{
var result = CalculateData();
}
catch (Exception ex)
{
// 何もしない(これはNG)
}
良い例(適切な処理):
try
{
var result = CalculateData();
}
catch (Exception ex)
{
// エラーの内容を記録し、必要なら上位に伝える
Console.WriteLine("エラーが発生しました:" + ex.Message);
throw; // 例外を上位に伝える(再スロー)
}
このように、最低限ログを出力するだけでも、後から原因を調べるのに大きく役立ちます。
また、場合によっては「このエラーは自分のところでは対処できない」と判断して、上位の処理(グローバル例外処理など)に例外を再スロー(throw;
)するのも良い対応です。
catchブロックの中にthrow;
だけを書くのは、何の処理もせずに例外を上位にそのまま伝えるだけなので、基本的には意味がありません。
再スローする場合は、せめてエラーの内容を記録したり、何かしらの追加処理を行ってからthrow;を使うようにしましょう。
詳細なログを残す
例外が発生したら、エラーの内容だけでなく、コードの何行目で起きたのか(スタックトレース)まで記録することが大切です。
具体的には以下のようなログ記録が望ましいです。
try
{
SaveToDatabase(data);
}
catch (Exception ex)
{
// 詳細なエラー情報をログに記録する
LogError(ex.ToString()); // ex.ToString()でスタックトレースも記録される
throw;
}
ログには詳細情報が含まれるため、後からエラーを調査する際に非常に役立ちます。
頻繁に発生しそうなエラーは事前チェックする
例外処理(try-catch)はとても便利ですが、例外が発生するとプログラムの動きが少し遅くなることがあります。
そのため、「よくあるエラー」や「事前に予測できるエラー」は、できるだけif文などで先に防ぐ工夫をするのがポイントです。
例えば、リストや配列の要素を取り出すときに、存在しないインデックスを指定すると例外が発生します。
こうした場合、あらかじめインデックスが範囲内かどうかを調べておくことで、余計な例外を防ぐことができます。
良くない例(例外に頼る場合)
try
{
var numbers = new List<int> { 1, 2, 3 };
Console.WriteLine(numbers[5]); // リストに存在しないインデックスを指定すると例外が発生する
}
catch (ArgumentOutOfRangeException ex)
{
Console.WriteLine("範囲外の要素です:" + ex.Message);
}
良い例(事前にチェックする場合)
var numbers = new List<int> { 1, 2, 3 };
var index = 5;
if (index >= 0 && index < numbers.Count)
{
Console.WriteLine(numbers[index]);
}
else
{
Console.WriteLine("指定されたインデックスは範囲外です。");
}
このように、よく発生するエラーはtry-catchに頼らず事前にif文などでチェックする習慣をつけておくと、プログラムが無駄に遅くなったり、予期しない動きになるのを防ぐことができます。
まとめ
今回は、C#における例外処理の基本や、実際の開発で役立つポイントを丁寧に解説しました。
try-catchの使い方やエラーごとの適切な対処方法を知っておくことで、プログラムの安全性や信頼性が大きく高まります。
初心者のうちから例外処理の良い習慣を身につけておくと、後々の開発でも必ず役立ちます。ぜひ今回紹介した内容を、自分のプログラムで少しずつ試してみてください。
次回は、最近のC#開発でますます重要になっている「非同期処理(async/await)」について、わかりやすく解説していきます。