例外処理と条件分岐の使い分け

例外処理と条件分岐の使い分け

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

今回は例外処理と条件分岐の使い分けについてです。明確な答えはないので、1つの参考にしてください。
また、C#を軸に話しますが、例外の仕組みを持つ他のプログラミング言語でも大抵が同じだと思います(たぶん)。

例外(Exception)とは

C#では実行時に発生するエラーを例外と言い、誰もcatchしないとプログラムが強制終了します。
そして、例外処理とは重い(遅い)処理に分類されます。それ故に使うか使わないかの判断で喧嘩が起こります。

管理人
管理人

こいつは最高の話題だ。切り込んでいこう!

りさ
りさ

うわぁ...

例外処理の遅さ

そもそも本当に遅いの?って疑問ありませんか。結論を言うと遅いです。
ただ、より正確な答えを伝えると「遅いけど遅くない」が答えです。

りさ
りさ

意味が分からない。

管理人
管理人

これからいくつか例を出すので、それを踏まえて考えましょう。

最初に、実際にどれくらいの差が出るかを確認します。


namespace Sample
{
  internal class Program
  {
    private const int N = 10000000;

    static void Main(string[] args)
    {
      var sw = new System.Diagnostics.Stopwatch();

      var array = new string[N];
      for (int i = 0; i < N; i++)
        array[i] = null;

      // 条件分岐で記述する
      sw.Start();
      for (int i = 0; i < N; i++)
      {
        if (array[i] != null)
          array[i].ToLower(); // null参照なら何でもいい
      }
      sw.Stop();

      Console.WriteLine($"{sw.ElapsedMilliseconds}ms");
      sw.Reset();

      // 例外処理で記述する
      sw.Start();
      for (int i = 0; i < N; i++)
      {
        try
        {
          array[i].ToLower(); // null参照なら何でもいい
        }
        catch (NullReferenceException e)
        {
        }
      }
      sw.Stop();

      Console.WriteLine($"{sw.ElapsedMilliseconds}ms");
      sw.Reset();
    }
  }
}

これはnull参照を条件分岐で回避するか例外処理で回避するかのプログラムです。
計測単位をms(表示が楽だから)にしたかったので、10,000,000回のループ処理にしました。
とんでもない数値なのは最近のPCが早すぎてmsに到達しなかったから。進化しゅごい。

結果は条件分岐が6ms、例外処理が35,669msになりました。

管理人
管理人

結構待たされたな。桁が大きいけど6,000倍くらいかな。だいぶ違うね。

りさ
りさ

やっぱ遅いじゃん。

管理人
管理人

これで例外処理を否定しちゃう人、めっちゃ浅いです。人生も浅すぎです。

りさ
りさ

そうやって答えを誘導して煽るのよくないよ(怒)。

では、次のサンプルです。同じく10,000,000回で実行します。

これはnull参照が起きないように空文字で初期化しました。
他は同じプログラムですが、今回はNullReferenceExceptionは起きません。


namespace Sample
{
  internal class Program
  {
    private const int N = 10000000;

    static void Main(string[] args)
    {
      var sw = new System.Diagnostics.Stopwatch();

      var array = new string[N];
      for (int i = 0; i < N; i++)
        array[i] = String.Empty; // null参照が起きないようにする

      // 条件分岐で記述する
      sw.Start();
      for (int i = 0; i < N; i++)
      {
        if (array[i] != null)
          array[i].ToLower(); // 参照なら何でもいい
      }
      sw.Stop();

      Console.WriteLine($"{sw.ElapsedMilliseconds}ms");
      sw.Reset();

      // 例外処理で記述する
      sw.Start();
      for (int i = 0; i < N; i++)
      {
        try
        {
          array[i].ToLower(); // 参照なら何でもいい
        }
        catch (NullReferenceException e)
        {
        }
      }
      sw.Stop();

      Console.WriteLine($"{sw.ElapsedMilliseconds}ms");
      sw.Reset();
    }
  }
}

結果は条件分岐が94ms、例外処理が95msになりました。

りさ
りさ

絶対に例外が起きないようにしたんだから早いに決まってるじゃん。

管理人
管理人

ポイントはそこじゃないよ。

気が付く人もいると思いますが、例外処理の部分をよく見ましょう。
これ別にtry-catchを外してないです。この違いがとても重要です。

つまり、例外処理はtry-catchで囲んでも、該当する例外が発生しない限りは遅くなりません。
これが「遅いけど遅くない」の答えです。なお、流石にノーガードと比べると少しは遅くなるとは思います。

管理人
管理人

ついでなのでノーガードも計測。結果は96msだったけど、たぶん誤差です。


namespace Sample
{
  internal class Program
  {
    private const int N = 10000000;

    static void Main(string[] args)
    {
      var sw = new System.Diagnostics.Stopwatch();

      var array = new string[N];
      for (int i = 0; i < N; i++)
        array[i] = String.Empty;

      // ノーガード
      sw.Start();
      for (int i = 0; i < N; i++)
      {
        array[i].ToLower();
      }
      sw.Stop();

      Console.WriteLine($"{sw.ElapsedMilliseconds}ms");
      sw.Reset();
    }
  }
}

例外の発生頻度

真面目に早い遅いを語るなら発生頻度を考慮して考えます。

例えば例外処理が発生した場合の処理時間が遅くても、その例外が発生する可能性が1/10,000なら気にする必要があるのでしょうか?
そもそも、一般的なプログラムはベンチマーク最速を目指す必要はありません。それよりもメンテナンス性を選ぶほうが賢明です。

と言うか、どうせ1/10,000でしか起きない時点で、それはイレギュラーです。そういった処理を効率重視で対策するのは無意味です。
さらに言うなら、先程の処理はループの全てで例外が発生した場合です。その時点で何かが致命的に間違ってます。

とは言え、ゲームのような最高パフォーマンスに意味があるプログラムなら、検討することに価値があるかもしれません。
僕はゲーム開発者じゃないのでリアルな現場は知りませんが、僕ならゲームを問わず遅いハードウェアなんて切り捨てますが。

管理人
管理人

こういうこと言うとスペックが低いPCでも動くプログラムは優れてるって人いるのよ。

りさ
りさ

低スペックでも動くなら優れてるでしょ。

管理人
管理人

それ、パソコンじゃなくてゴミだから。早く捨てたほうがいいよ。

りさ
りさ

性能に厳しすぎ。

例外処理で記述するメリット

ここまで言うのは例外処理にメリットがあるからです。後、先に言っておくと大切なのは割合です。
何でもかんでも例外処理、全部を条件分岐って考え方が1番の間違いです。

例えば、こんな感じに正しい限りは処理が進み、例外が発生するとcatchに流れるように書きます。
こうするとError時の処理を下側に統一できたり、任意にthrowを使うことで例外をコントロールできます。


namespace Sample
{
  internal class Program
  {
    static void Main(string[] args)
    {
      try
      {
        // 例外が発生するかもしれない処理
        if (false) // テスト用: コメントアウトで実行をコントロール
        {
          throw new NullReferenceException();
        }

        // 例外が発生するかもしれない処理
        if (false) // テスト用: コメントアウトで実行をコントロール
        {
          throw new InvalidCastException();
        }

        // 例外が発生するかもしれない処理
        if (false) // テスト用: コメントアウトで実行をコントロール
        {
          throw new FormatException();
        }
      }
      catch (NullReferenceException e)
      {
        // null参照した時の処理
      }
      catch (InvalidCastException e)
      {
        // キャスト失敗した時の処理
      }
      catch (FormatException e)
      {
        // Formatが駄目な時の処理
      }
    }
  }
}

他にも、こんな感じに複数箇所で同種の例外を発生させます。
そうすると発生した種類と同じErrorの対策は同一箇所に記述できます。


namespace Sample
{
  internal class Program
  {
    static void Main(string[] args)
    {
      try
      {
        // 例外が発生するかもしれない処理
        if (false) // テスト用: コメントアウトで実行をコントロール
        {
          throw new FormatException();
        }

        // 例外が発生するかもしれない処理
        if (false) // テスト用: コメントアウトで実行をコントロール
        {
          throw new FormatException();
        }

        // 例外が発生するかもしれない処理
        if (false) // テスト用: コメントアウトで実行をコントロール
        {
          throw new FormatException();
        }
      }
      catch (FormatException e)
      {
        // Formatが駄目な時の処理
      }
    }
  }
}

僕はコードのメンテナンス性は非常に大切だと思っていて、サラリーマン系プログラマとして歩むなら1番重視する部分です。
はっきり言って、パフォーマンスなんて、どうでもいいです。それよりもメンテナンス性。後から改造しやすいプログラムこそが神です。

まず、普通の業務なら作り終わったら2度と触らないってパターンは少ないです。と言うか無いです。
仮に契約がそうなっている場合でも、ゴネゴネ赤ちゃんに絡まれると触らざるを得ません。

管理人
管理人

むしろパフォーマンス無視して作って、後の改造で高速化できる余地を残してるほうがビジネス優秀マン。

りさ
りさ

賢い...

例外処理で記述するデメリット

伝えた通り処理の遅さはデメリットと思ってません。じゃあ、何があるのって感じですが、C#の場合は独自例外を作るのがめんどいです。
例外ってその名前に凄く価値があって、ほぼ全てのプログラマは例外の名前から発生した問題を想像します。

つまり、見たことの無い例外(独自)とか、事象と合ってない例外が発生すると戸惑います。合ってないに関しては殴られます。
独自例外とか任意のthrowって、それらを満たさないような実装ができるので、真剣に作らないと混乱の元になるんです。

例えば該当する例外がなく、最上位のException型なら全対応でヨシ!って感じでthrowすると、無事やべぇプログラムが誕生します。

りさ
りさ

後で苦労するの自分では?

管理人
管理人

やべぇ、プログラムを誰かにパスしてこそプロのサラリーマン。

りさ
りさ

最悪だ...

参考にすべき例外処理と条件分岐

そうは言っても「何か参考になるコードないの?」とか思うじゃないですか。
ありますよ。無限とは言いませんが、案外たくさんあります。

現在はOSS(オープンソース)が基準になったことで、有名なアプリやLibraryでも公開される時代になりました。
特にLibraryなんかは、非公開Libraryだと選択肢から除外されることもあり、OSSの優位性を圧倒的に感じます。

管理人
管理人

内部で何してるか分からないLibraryより、処理が見えるLibraryのほうが安心だよ。

りさ
りさ

資産であるソースコードを全て公開って凄いね。

管理人
管理人

個人ならまだしも会社もOSSの時代だからな。凄まじいよ。

と言うことで、何か参考にしたいと思ったら、同じ言語の有名アプリとかLibraryを参考にすればOK。
C#なら最強Libraryの.NETGitHubに公開されてます。Libraryを作る場合は真っ先に参考にすべき情報でしょう。

あとがき

これも1つの意見です。参考にするもしないも個人の自由ってことで。
この業界、しょうもない宗教問題が多いので最後は自分で考えたほうがいいですよ。

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

関連記事

コメント

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