C# 例外処理の基礎と実践 ― 初学者から一歩進んだ安全設計へ
対象読者
- C#/Unity を学び始めた初学者〜中級者
- 「try-catch は知っているけど、正しい使い方や設計指針があいまい」と感じている方
- 技術ブログや教材用のまとまった資料を探している教育者
この記事でわかること
- 例外処理の目的と基本文法
- finally と using の正しい使い分け
- 例外フィルターや再スローなど、知っておきたい応用テクニック
- カスタム例外の設計指針
- Unity 実装で気をつけたいポイント
- 練習問題と模範解答(ダウンロードリンク付き)
目次
- 1. 1. なぜ例外処理が必要か
- 2. 2. 基本文法 ― try-catch-finally
- 3. 3. finally と using ― リソースの寿命を保証する
- 4. 4. 例外フィルター (catch when) で条件分岐
- 5. 5. 例外を再スローする 3 つのパターン
- 6. 6. カスタム例外の設計指針
- 7. 7. 例外処理のアンチパターン集
- 8. 8. Unity での例外処理 Tips
- 9. 9. 練習問題
- 10. No. 1 名前チェック(★)
- 11. No. 2 0 除算リトライ(★★)
- 12. No. 3 CSV 読み込み&ラップ例外(★★★)
- 13. No. 4 Web 取得のタイムアウト検知(★★★)
- 14. 10. まとめ ― “落ちても壊れない” コードを書くために
1. なぜ例外処理が必要か
- エラーを「検知」し、プログラムの暴走を防ぐ例外は “プログラムが想定外の状態に陥った” ことを示すシグナル。放置するとクラッシュやデータ破損に直結します。
- 「正常系」と「異常系」を分離し、読みやすいコードを保つtry ブロックには本来のロジックだけを書くことで、メンテナンス性が向上。
- リソースリークを防ぐファイルやネットワーク接続を確実に開放する仕組みとして finally/using が不可欠です。
2. 基本文法 ― try-catch-finally
try
{
// 1. 例外が発生する “かもしれない” 処理
var text = File.ReadAllText("data.txt");
Console.WriteLine(text);
}
catch (FileNotFoundException ex) // 2. 具体的な型から先に並べる
{
Console.WriteLine($"ファイルが見つかりません: {ex.FileName}");
}
catch (Exception ex) // 3. 最後に最も広い型
{
Console.WriteLine($"予期せぬエラー: {ex.Message}");
throw; // 4. 上位へ再スロー(ログ後に伝搬)
}
finally
{
Console.WriteLine("必ず実行:ログを残すなど");
}
書き方のポイント
やること | 理由 |
---|---|
具体的な例外型→一般的な型 の順で catch を並べる | 早期ハンドリングで誤捕捉を防ぐ |
業務上リカバリできる場合のみ 例外を握りつぶす | 理由のない catch {} はバグ隠しになる |
throw; で 情報を保ったまま再スロー | throw ex; はスタックトレースを失うため NG |
3. finally と using ― リソースの寿命を保証する
3-1. finally ブロック
finally 内のコードは 例外の有無に関係なく必ず実行 されます。主な用途は「手動で Dispose/Close したいケース」。
StreamReader reader = null;
try
{
reader = new StreamReader("data.txt");
Console.WriteLine(reader.ReadToEnd());
}
finally
{
reader?.Dispose(); // 明示的に後処理
}
3-2. using ステートメント/宣言(C# 8.0 以降)
- ステートメント版(従来から存在)
using (var reader = new StreamReader("data.txt"))
{
Console.WriteLine(reader.ReadToEnd());
} // ここで自動 Dispose
- 宣言版(C# 8.0~)― スコープ終端で自動 Dispose
using var conn = new SqlConnection(connStr);
conn.Open();
使い分けの指針
- ひとつのメソッド内で完結 → using
- 何段にもネストしたり、2 か所以上で確実に開放したい → finally
4. 例外フィルター (catch when) で条件分岐
catch (SqlException ex) when (ex.Number == 547) // 外部キー違反のみ捕捉
{
Console.WriteLine("必須項目が不足しています。");
}
- catch の後ろで 捕捉するかどうかを動的に判定
- フィルターに合致しなければ、その catch をスキップして次のハンドラへ
5. 例外を再スローする 3 つのパターン
パターン | 用途 | 例 |
---|---|---|
throw; | 元の例外を維持 | ログを取ったあと上位へ伝搬 |
throw new CustomException(“追加情報", ex); | 文脈をラップ | DAL → BLL へドメイン固有の意味を付与 |
return や false 等で 例外自体を封じる | リカバリ可・ユーザ通知不要 | TryParse パターンなど |
6. カスタム例外の設計指針
[Serializable]
public class InvalidScoreException : Exception
{
public int Score { get; }
public InvalidScoreException(int score)
: base($"不正なスコア値 ({score}) です。0~100 の範囲で入力してください。")
{
Score = score;
}
protected InvalidScoreException(
SerializationInfo info, StreamingContext ctx) : base(info, ctx)
{
Score = info.GetInt32(nameof(Score));
}
}
- Exception を継承(ApplicationException は非推奨)
- 4 つの標準コンストラクタを用意(直列化対応も忘れずに)
- “○○できない”“××が無効” など、原因が明確な命名にする
7. 例外処理のアンチパターン集
アンチパターン | 問題点 | 改善策 |
---|---|---|
catch (Exception ex) { } | エラーを隠蔽しデバッグ不能 | ログ & 再スロー/適切な型で捕捉 |
ロジックの 9 割を try に包む | 正常系と異常系が混在し読みにくい | 例外が起き得る最小範囲を包む |
戻り値と例外を混在 | API の利用者が混乱 | “成功時は値、失敗時は例外”を徹底 |
8. Unity での例外処理 Tips
シーン | 注意点 |
---|---|
Coroutine | IEnumerator 内で投げた例外はメインスレッドに伝搬。try-catch で囲い、Debug.LogError で可視化を |
Update ループ | 毎フレーム呼ばれるメソッドに多用すると FPS 低下の原因。重い処理は Start/Awake など一度きりの場所へ |
非同期 (async/await) | await の直前後を try-catch で囲む。タスクの戻り値を忘れず ConfigureAwait(false) は Unity では非推奨 |
9. 練習問題
No. | 内容 | 難易度 |
---|---|---|
1 | 名前を入力させ、空文字なら ArgumentException を投げる | ★ |
2 | 0 除算の例外を catch when でフィルターし、リトライ回数を 3 回までに制限 | ★★ |
3 | CSV 読み込みクラスを実装し、I/O エラーは CsvLoadException(カスタム)にラップ | ★★★ |
4 | async Task メソッドで WebAPI 呼び出し。タイムアウト時に独自の NetworkTimeoutException を再スロー | ★★★ |
以下に No. 1〜4 の模範解答コードをそれぞれ示します。すべて C# 12 / .NET 8 でそのままビルド・実行できます。
No. 1 名前チェック(★)
using System;
class Program
{
static void Main()
{
Console.Write("名前を入力してください: ");
var name = Console.ReadLine();
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("名前は空にできません。", nameof(name));
Console.WriteLine($"こんにちは、{name}さん!");
}
}
No. 2 0 除算リトライ(★★)
using System;
class Program
{
static void Main()
{
int retries = 0;
while (true)
{
try
{
Console.Write("分子を入力: ");
double numerator = double.Parse(Console.ReadLine());
Console.Write("分母を入力: ");
double denominator = double.Parse(Console.ReadLine());
double result = Divide(numerator, denominator);
Console.WriteLine($"結果: {result}");
break; // 成功したらループを抜ける
}
catch (DivideByZeroException) when (retries < 2)
{
retries++;
Console.WriteLine("0 で割ることはできません。もう一度入力してください。");
}
}
}
static double Divide(double x, double y) => x / y;
}
No. 3 CSV 読み込み&ラップ例外(★★★)
using System;
using System.Collections.Generic;
using System.IO;
namespace CsvLoader
{
// 独自例外
public class CsvLoadException : Exception
{
public CsvLoadException(string path, Exception inner)
: base($"CSV の読み込みに失敗しました: {path}", inner) { }
}
// CSV 読み込みユーティリティ
public static class CsvReader
{
public static List<string[]> Load(string path)
{
try
{
var rows = new List<string[]>();
foreach (var line in File.ReadLines(path))
rows.Add(line.Split(','));
return rows;
}
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
{
// I/O 系エラーだけラップして上位へ
throw new CsvLoadException(path, ex);
}
}
}
class Program
{
static void Main()
{
try
{
var rows = CsvReader.Load("sample.csv");
Console.WriteLine($"行数: {rows.Count}");
}
catch (CsvLoadException ex)
{
Console.WriteLine(ex.Message);
}
}
}
}
CSVプロジェクトと同じ階層に sample.csv を用意してください(カンマ区切りテキスト)。
No. 4 Web 取得のタイムアウト検知(★★★)
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace NetworkTimeoutSample
{
// タイムアウト専用の独自例外
public class NetworkTimeoutException : Exception
{
public NetworkTimeoutException(string url, Exception inner)
: base($"タイムアウトしました: {url}", inner) { }
}
class Program
{
static async Task Main()
{
try
{
string html = await FetchAsync(
"https://example.com",
timeout: TimeSpan.FromSeconds(2));
Console.WriteLine(html[..Math.Min(html.Length, 100)]); // 先頭 100 文字だけ表示
}
catch (NetworkTimeoutException ex)
{
Console.WriteLine(ex.Message);
}
}
public static async Task<string> FetchAsync(string url, TimeSpan timeout)
{
using var cts = new CancellationTokenSource(timeout);
try
{
using var client = new HttpClient();
return await client.GetStringAsync(url, cts.Token);
}
catch (TaskCanceledException ex) when (cts.IsCancellationRequested)
{
throw new NetworkTimeoutException(url, ex);
}
}
}
}
ネットワーク環境によっては 2 秒以内に応答が返る場合があります。時間を伸ばすか URL を変えて試してみてください。
10. まとめ ― “落ちても壊れない” コードを書くために
- 例外は「異常系」の入り口。正常系と分離して読みやすさを保つ
- リソースは確実に解放― using と finally を適材適所で
- 捕捉は最小限・ログは詳細に・再スローで文脈を維持
- Unity ではパフォーマンスも意識。フレームループの中で多用しない
- アンチパターンは “臭い” に敏感に。握りつぶし/巨大な try には要注意
「落ちても大丈夫な設計」を最初に組み込むことで、バグ調査・デバッグコストは 必ず 下がります。
ぜひ小さなサンプルから例外処理の“筋トレ”を積み重ねてみてください。
訪問数 3 回, 今日の訪問数 1回
ディスカッション
コメント一覧
まだ、コメントがありません