例外処理、try-catch文の知識を深めよう!

[C#] 例外処理、try-catch文の知識を深めよう!

※ 当サイトは広告を含みます。

今回はC#の例外処理について知識を深めます。
残念ながら前回の内容は実用レベルだと不十分で、本格的なプログラムを作る場合は知識が足りません。

例外(Exception)

最初におさらいです。C#では実行時に発生するエラーを例外と言います。
そして、この例外を誰もcatch(キャッチ)しない場合にプログラムが強制終了します。

また、この例外処理ですが、一般的に重い処理(コストが高いと言う)に分類されます。
つまりは極論になりますが、例外処理で対策するよりも事前の分岐処理で逃げたほうが速度有利です。

じゃあ、例外は使わないほうが良いのか? みたいな話になりますが、そんなことはありません。
例外にもメリット・デメリットがあるので、それらを理解して使いましょう。ここは本題とズレるので別にします。

catch文にはException型を指定する

既に覚えた記述はこんな感じですが、こういうcatch文は殆どありません。


namespace Sample
{
  internal class Program
  {
    static void Main(string[] args)
    {
      string s = Console.ReadLine();

      try
      {
        int n = int.Parse(s); // 例外の可能性がある

        Console.WriteLine(n);
      }
      catch
      {
        // 例外が発生した場合の処理
        Console.WriteLine("例外が発生しました。");
      }
    }
  }
}

普通は、こんな感じにException型を指定します。


namespace Sample
{
  internal class Program
  {
    static void Main(string[] args)
    {
      string s = Console.ReadLine();

      try
      {
        int n = int.Parse(s); // 例外の可能性がある

        Console.WriteLine(n);
      }
      catch (Exception e)
      {
        // 変数eには例外の情報が入ってる
        Console.WriteLine($"{e.Message}");
      }
    }
  }
}

このようにException型と一緒に変数を宣言すると、発生した例外の情報が変数に入ります。
後、当たり前ですがfinally文も書けます。部分的に抜粋すると、こんな感じです。


try
{
}
catch (Exception e)
{
}
finally
{
}

Exception型

例外には種類があり、それに応じたクラスが存在します。そして、Exception型は例外の最上位クラスです。
つまり、全ての例外はException型またはException型を派生したクラスになります。

https://learn.microsoft.com/en-us/dotnet/api/system.exception

また、Exception型を派生したクラスは命名規則で、必ず〇〇Exceptionって名前になります。
先に伝えると、C#では独自の例外を作成することもでき、その場合も〇〇Exceptionって名付けるのがルールです。

管理人
管理人

付けなくてもビルドは通りますが、いわゆるゴミプログラムです。

Exception型の知識

Exception型には例外に関する情報が記録されていて、所有するプロパティも大半がその情報を表現します。
仮にException型を派生した例外があったら、その例外に対する情報が付加情報として増えるイメージです。

以下はException型において、よく利用するプロパティです。このへんの値を適用に出力しておけば大抵は何とかなります。

Message 例外に関するメッセージ
StackTrace メソッドの呼び出し履歴
TargetSite 例外が発生したメソッド
Source 原因になったオブジェクトの名前
InnerException 内包するExceptionインスタンス

InnerExceptionは内包する例外の情報です。例外を発生させた後、誰もcatchせずに次の例外が発生することがあります。
そうした場合、直前の例外がInnerExceptionに格納されます。ちなみに存在しない場合の値はnullになります。


namespace Sample
{
  internal class Program
  {
    static void Main(string[] args)
    {
      string s = Console.ReadLine();

      try
      {
        int n = int.Parse(s);
        Console.WriteLine(n);
      }
      catch (Exception e)
      {
        Console.WriteLine($"{e.Message}");
        Console.WriteLine($"{e.StackTrace}");
        Console.WriteLine($"{e.TargetSite}");
        Console.WriteLine($"{e.Source}");

        if (e.InnerException != null)
        {
          Console.WriteLine($"{e.InnerException.Message}");
          Console.WriteLine($"{e.InnerException.StackTrace}");
          Console.WriteLine($"{e.InnerException.TargetSite}");
          Console.WriteLine($"{e.InnerException.Source}");
        }
      }
      finally
      {
      }
    }
  }
}

InnerExceptionException型です。つまり、InnerExceptionInnerExceptionを持つことがありえます。
よって、正しく記述するならwhile文とかを使い、内包するInnerExceptionを全て出力します。


namespace Sample
{
  internal class Program
  {
    static void Main(string[] args)
    {
      string s = Console.ReadLine();

      try
      {
        int n = int.Parse(s);
        Console.WriteLine(n);
      }
      catch (Exception e)
      {
        Exception tmp = e;
        while (tmp != null)
        {
          Console.WriteLine($"{tmp.Message}");
          Console.WriteLine($"{tmp.StackTrace}");
          Console.WriteLine($"{tmp.TargetSite}");
          Console.WriteLine($"{tmp.Source}");

          tmp = tmp.InnerException;
        }
      }
      finally
      {
      }
    }
  }
}
管理人
管理人

ぶっちゃけ、Messageだけ読めば大体なんとかなる。

一般的なException型

普通にプログラムを作ってると、頻繁に出会うException型があることに気が付きます。
以下は、みんな知ってる系のException軍団です。この辺を覚えておけば良さげ。

NullReferenceException null参照
InvalidCastException 駄目なキャスト
IndexOutOfRangeException 範囲外参照
FormatException 何かフォーマットあってない
ArgumentException メソッドの引数が駄目
OverflowException オーバーフロー発生
TimeoutException Timeout発生
管理人
管理人

もっとあるけどキリがないのでここまで。

独自のException型

先に書いた通り、C#では独自の例外を作成できます。ただし、殆どの場合は.NET側の既存例外を使ったほうが楽です。
自作する場合はException型と同じ感覚で利用できるように、複数のコンストラクタを書きます。この辺がめんどいです。


internal class CustomException : Exception
{
  public CustomException()
  {
  }

  public CustomException(string? message)
    : base(message)
  {
  }

  public CustomException(string? message, Exception? innerException)
    : base(message, innerException)
  {
  }

  // 独自のプロパティとかメソッドを作る
}
りさ
りさ

この独自例外って、どうやったら発生するんですか?

管理人
管理人

それ専用の構文があるよ。方法は後で書くね。

複数の例外をcatchする

さて、catch文にはException型を記述すると伝えましたが、これは任意のException型を指定できます。
Exception型とは例外の最上位クラスのため、Exception型を記述する限りは全ての例外を捕捉できます。

対して、Exception型を派生したクラスを記述すると、それを満たす例外しか捕捉しなくなります。
例として、以下のようにFormatException型のみを記述すると、それを満たす例外しか捕捉しません。


namespace Sample
{
  internal class Program
  {
    static void Main(string[] args)
    {
      string s = Console.ReadLine();

      try
      {
        int n = int.Parse(s);
        Console.WriteLine(n);
      }
      catch (FormatException e)
      {
      }
    }
  }
}

ですが、これだとFormatException型に関する例外しか補足できません。
他の例外、例えばNullReferenceException型とかが発生すると死にます。

それでは困るので、普通は複数のcatch文を書いて、対応すべき全ての例外を処理します。


namespace Sample
{
  internal class Program
  {
    static void Main(string[] args)
    {
      string s = Console.ReadLine();

      try
      {
        int n = int.Parse(s);
        Console.WriteLine(n);
      }
      catch (FormatException e)
      {
        // FormatException型が発生した場合の処理
      }
      catch (NullReferenceException e)
      {
        // NullReferenceException型が発生した場合の処理
      }
    }
  }
}

こうすることで目的の例外が発生した場合の処理を分岐できます。
例えば入力値が問題なら、それをユーザーに教えて直してもらうとかできます。

この時の注意点ですが、複数のcatch文を書く場合、先に条件を満たす例外を記述することはできません。
つまり、こういうことです。この場合は常にException型を満たすため、絶対に後半に進みません。


namespace Sample
{
  internal class Program
  {
    static void Main(string[] args)
    {
      string s = Console.ReadLine();

      try
      {
        int n = int.Parse(s);
        Console.WriteLine(n);
      }
      catch (Exception e)
      {
      }
      catch (FormatException e)
      {
      }
    }
  }
}
管理人
管理人

こういうのはビルドエラーになります。解決先は単に順番を入れ替えればいいです。

throw文

C#には独自の例外を含めて、任意に例外を発生(投げると言う)させる方法があります。
それがthrow文です。構文は以下となり、発生させたい例外のインスタンスを生成します。


throw new 例外インスタンス;

普通はこんな感じに何らかのメッセージを記述します。


namespace Sample
{
  internal class Program
  {
    static void Main(string[] args)
    {
      try
      {
        throw new Exception("任意に例外を発生");
      }
      catch (Exception e)
      {
        Console.WriteLine(e.Message);
      }
    }
  }
}

インスタンスを生成せず既存の例外を投げることもできます。


namespace Sample
{
  internal class Program
  {
    static void Main(string[] args)
    {
      try
      {
        try
        {
          throw new Exception("任意に例外を発生");
        }
        catch (Exception e)
        {
          // catchで捕捉した例外を投げることも可能
          throw; // 例外インスタンスは指定しない

          // catchの中で元の例外インスタンスを指定すると情報が上書きされる。
          // この書き方はビルド時に警告が出るため、基本的に使う必要はない。
          //throw e;
        }
      }
      catch (Exception e)
      {
        Console.WriteLine(e.Message);
      }
    }
  }
}
管理人
管理人

独自例外もthrowを使って発生させることができます。

あとがき

例外発生時のポイントは、その後もプログラムを継続するか否かです。一般的に回復が可能なら、それに準じる処理を行い継続します。
それが無理ならログ等を残した後、問題となる事象をユーザーに伝え、自発的にプログラムを終了します。

管理人
管理人

1番駄目なのは、正しく回復できてないのに処理を進めること。こういうことをすると後に甚大な被害を生みます。

りさ
りさ

諦めるのも大切ってことだね。

◆ C#に関する学習コンテンツ

この記事は参考になりましたか?

関連記事

コメント

この記事へのコメントはありません。