C# 例外処理の基礎と実践 ― 初学者から一歩進んだ安全設計へ

対象読者

  • C#/Unity を学び始めた初学者〜中級者
  • 「try-catch は知っているけど、正しい使い方や設計指針があいまい」と感じている方
  • 技術ブログや教材用のまとまった資料を探している教育者

この記事でわかること

  1. 例外処理の目的と基本文法
  2. finally と using の正しい使い分け
  3. 例外フィルターや再スローなど、知っておきたい応用テクニック
  4. カスタム例外の設計指針
  5. Unity 実装で気をつけたいポイント
  6. 練習問題と模範解答(ダウンロードリンク付き)

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));
    }
}
  1. Exception を継承(ApplicationException は非推奨)
  2. 4 つの標準コンストラクタを用意(直列化対応も忘れずに)
  3. ○○できない”“××が無効” など、原因が明確な命名にする

7. 例外処理のアンチパターン集

アンチパターン問題点改善策
catch (Exception ex) { }エラーを隠蔽しデバッグ不能ログ & 再スロー/適切な型で捕捉
ロジックの 9 割を try に包む正常系と異常系が混在し読みにくい例外が起き得る最小範囲を包む
戻り値と例外を混在API の利用者が混乱“成功時は値、失敗時は例外”を徹底

8. Unity での例外処理 Tips

シーン注意点
CoroutineIEnumerator 内で投げた例外はメインスレッドに伝搬。try-catch で囲い、Debug.LogError で可視化を
Update ループ毎フレーム呼ばれるメソッドに多用すると FPS 低下の原因。重い処理は Start/Awake など一度きりの場所へ
非同期 (async/await)await の直前後を try-catch で囲む。タスクの戻り値を忘れず ConfigureAwait(false) は Unity では非推奨

9. 練習問題

No.内容難易度
1名前を入力させ、空文字なら ArgumentException を投げる
20 除算の例外を catch when でフィルターし、リトライ回数を 3 回までに制限★★
3CSV 読み込みクラスを実装し、I/O エラーは CsvLoadException(カスタム)にラップ★★★
4async 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. まとめ ― “落ちても壊れない” コードを書くために

  1. 例外は「異常系」の入り口。正常系と分離して読みやすさを保つ
  2. リソースは確実に解放― using と finally を適材適所で
  3. 捕捉は最小限・ログは詳細に・再スローで文脈を維持
  4. Unity ではパフォーマンスも意識。フレームループの中で多用しない
  5. アンチパターンは “臭い” に敏感に。握りつぶし/巨大な try には要注意

「落ちても大丈夫な設計」を最初に組み込むことで、バグ調査・デバッグコストは 必ず 下がります。

ぜひ小さなサンプルから例外処理の“筋トレ”を積み重ねてみてください。

訪問数 3 回, 今日の訪問数 1回

C#,例外

Posted by hidepon