読者です 読者をやめる 読者になる 読者になる

Kokudoriing

技術系与太話ブログ

例外とは何か

プログラミング

大抵のプログラミング言語には例外機構がサポートされています。
しかし、例外とは何でしょう。
「例外」という言葉からはなんとなく「ダメだった」、「失敗した」といった印象を感じられます。
つまり、「イレギュラー」な事。「異常」なことといった印象。

「異常」があれば「正常」があるわけで、では「正常」とは何なのか。
だんだんと禅問答のような感じになってきつつありますが、非常に大切な考えです。
例えば、最近の開発手法にビヘイビア駆動開発(BDD)があります。
これは単一メソッドといった粒度での振る舞い(ビヘイビア)を定義し、
そのビヘイビアを達成できるようにプログラマが実装しようという考え方です。

つまり、BDDは最初に「正常系」を定義し、「正常系」の差集合から「異常系」を導出しています。
BDDが最近注目を浴びているのは、以前は先に「異常系」を定義していたからであり、
先に「正常系」を定義するBDDの方法の方が理にかなっているからです。

同じことが例外にも言えます。
つまり、各々のメソッドにはあるべき「正常系」を必ず持っています。
つまり、例外とはメソッドが自身の「正常系」から外れ、「異常系」へと落ちた状態で発生します。

これは至極簡単で、
Stream File.Open(string fileName) というメソッドがあるとすると、
「fileName パスからファイルを探し、内容をストリームとして返す」事が「正常系」だと考えられます。
なので、この正常系の一部でも満たすことができなければこのメソッドは例外を発生させるべきです。

さて、ここで重要なのはメソッドは実装者と呼び出し者という2人の人間が関係するということです。
メソッドの正常系を定義するのは実装者の責任でしょう。
では、呼び出し者はどうやってその正常系を知ればいいのでしょうか。

これには様々な方法があります。
最も単純なのは実装者が呼び出し者に直接言うことです。
これをより効率化したものがドキュメントであり、各種補助ドキュメント機構です。

しかし、Stream File.Open(string fileName) という先ほど例に上げたメソッドの場合、
ドキュメントなど無くても「fileName パスからファイルを探し、内容をストリームとして返す」事が正常系だと察することは出来ます。
Open という名前のメソッドが File クラスに紐付かれているのだから、ファイルを開くわけです。
引数に fileName を要求し、Stream を返すのだから、fileName パスからファイルを探し、内容をストリームとして返すわけです。

つまり、呼び出し者はメソッドのシグニチャを元にメソッドの正常系を類推するわけです。
「プログラミング .NET Framework」では、

例外とは、あるメンバーが、その名前から期待される処理を完了できなかったときに起こります。
プログラミング .NET Framework 第3版 p484

とあります。
ここではより一般化し、
「例外とは、あるメンバーが、そのシグニチャから期待される処理を完了できなかったときに起こる」
とします。

プログラミング .NET Framework  第3版 (マイクロソフト公式解説書)

プログラミング .NET Framework 第3版 (マイクロソフト公式解説書)

さて、先ほどメソッドは実装者と呼び出し者の2人が関係すると言いました。
実装者はシグニチャから期待できる処理を適切に実装する責任を持ちます。
そして、呼び出し者はメソッドは引数や呼び出し状況などを適切に処理しなければならない責任を持ちます。
どちらか1つ以上の責任がないがしろにされた時、メソッドは例外を発生させなければなりません。
両者が協調し責任と利益を交換するので、これを契約として考えることができます。

Stream File.Open(string fileName) は呼び出し者が適切なファイル名を渡すことで、
実装者が適切なストリームを返すという契約そのものだという考え方です。
このような考え方を契約プログラミングと言います。
また、呼び出し者の責任を事前条件、実装者の責任を事後条件と言います。
事前条件、事後条件が破られた時、契約が違反されたものとしてメソッドは例外を投げなければなりません。


しかし、ここで疑問が生じます。
メソッドが正しく例外を投げたとします。
さて、呼び出し側はそんなものを投げられてどうすればいいのでしょうか。

File.Open が何故か失敗したようです。
しかし、それはOSレベルの問題かもしれません。
権限の問題かもしれません。
そもそも要求するファイルが既に消えているのかもしれません。
だからといって、呼び出し側に出来ることが何かあるでしょうか?

例外は大きく2つに分類することができます。
解決不可能な例外と、解決可能な例外の2つです。

例えば File.Open による例外のほとんど全ては解決できない問題です。
要求するファイルパスがなかったとして、それを解決することはできません。

しかし、世の中には解決可能な例外もまた存在します。
例えば File.Open をロギングの為に使用していたとします。
もし要求するファイルパスが存在しない例外が投げられれば、
勝手にファイルを作ってしまえばいいのです。

そして、解決不可能な例外は、プログラマーの責任である場合と
プラットフォームである場合の2種類があります。

プログラマーの責任である解決不可能な例外は先ほどの事前条件違反が顕著です。
例えば引数に null を入れてはいけないのに呼び出し者が null を入れたしまった。
これは明確な事前条件違反なのでメソッドは ArgumentNullException 的な例外を発生させる必要があります。
ここでこの ArgumentNullException を catch したからといって何も出来ません。
この例外は catch せずに実行時例外を発生させましょう。
するとそれに気づいた呼び出し者が事前条件に違反していることを気づくことができ、
適切な呼び出しに変更することができます。

プラットフォームの責任である解決不可能な例外は OutOfMemoryException などの例外です。
OutOfMemoryException はコンピュータ上にメモリが不足することで発生する例外です。
こんな例外を catch したとしても、動的にメモリを追加することは出来ません。
この例外の解決法はユーザーに物理的にメモリを増やしてもらうことくらいです。

既に説明したように解決可能な例外があります。
基本的に例外を catch する必要のある例外はこの種類の例外のみです。
また、いかなる例外だとしても catch(Exception e) {} 等で握りつぶすことは基本的に悪手です。

そういえば、少し前に「イベント目的に例外を使用しても問題ないか?」といった質問を受けました。
これは絶対に避けるべきです。
理由は2つあります。

まず第一に例外は「例外」のために存在する機構であり、
それ以外のために使用すべきではありません。
仮に例外を「例外」と「イベント」のために使用すると、
現在扱っている例外機構が「例外」のためなのか「イベント」のためなのかをいちいち把握する必要が生じます。
これは不用意に複雑性を注入しているだけなのでするべきではありません。

第二に、例外は基本的に重い処理です。
IO処理に比べるとかなり軽いですが、
普通のメソッド呼び出しに比べるとかなり重い、程度の認識でいいかと思います。


さて、まとめますと。
例外はシグニチャから期待できる処理が出来なかった場合に発生させる。
メソッドのシグニチャが要領を得ない場合、例外も要領を得ない感じになり最悪。
例外の種類に基づいて適切に例外を処理する。
例外を何でもかんでもキャッチしない。
例外機構を例外の意味以外で使用してはいけない。