.NETの例外処理 Part.1
さて次は何を書こうかなぁと思ってましたが、前回のエントリのウケが比較的よかった様子なので、もうちょっと初心者向けのエントリを続けてみようと思ったり。ということで、今回は .NET の例外処理について書いてみたいと思います。
なぜにいまさら例外処理……? と思われる方も多いと思うのですが、理由はただ一点。現場レベルのソースコード読んでると、未だに例外処理がめちゃくちゃなアプリがホントに多いのです。
私が昔、Java を初めて触ったときに感動した機構の一つが例外処理(とそれに関連するスタックトレース)で、例外処理を正しく書くだけで、アプリの内部動作の大部分はわかるし、障害解析も圧倒的にラクになる。しかしその一方で、例外処理についてわかりやすくまとめられた書籍が少ないために、なかなか理解するのが大変なところでもあるんですよね。自分の書いた開発技術大全 vol.3 だけではどうもまだ分かりにくいようなので、少し加筆しながらここで説明しよう、と思った次第です。そんなわけで、今日は比較的初心者向けのトピックですが、気楽にお付き合いいただけると嬉しいかもです。
# といいつつ、たぶんそんなに気楽に読めるエントリでもないかもしれません。
# 例外処理は、実はアプリケーションデザインにもかかわる部分があり、例えば BC や DAC の
# メソッドの引数・戻り値の設計にも影響を与えてくるからです。なので、一度じっくり腰を据えて学習
# してみてください。一度きちんと勉強すれば済む話ですので^^。
まず、そもそも .NET アプリケーションにおける「例外」は、どのような状況を表現するために用意されている機能なのか、というところから解説していくことにします。
[.NET アプリケーションにおける例外と業務エラーの違い]
一般的に、業務アプリケーションを開発する場合には、業務フローチャートを作成し、その流れを C# や VB のコードによって実装します。

通常、業務処理の終了(=各メソッドの終了)は、正常終了と業務エラーのパターンに大別されますが、例外はこうした正常終了や業務エラーを表現するために利用してはいけません。例外はその名の通り、例外的な状況、すなわち業務フローチャートからはみ出してしまったという、想定外の事象を表現するために利用します。
具体的には、データベース接続エラーやネットワークエラー、メモリ不足などが発生した場合には、この状況を例外によって表現する、というのが .NET における大原則、と理解してください。
しかし、この例だけだと分かりにくいと思いますので、もうちょっと具体的な例を取り上げて説明してみましょう。例えば、重複ユーザ ID の利用を認めないような、新規顧客登録業務を考えてみます。
このような業務アプリにおいて、どのようなケースが業務エラーに相当し、どのようなケースが例外に相当するのかを考えてみます。ボタンを押したあとの応答は、以下の 3 つのパターンに分類できますが、.NET アプリケーションでは、③のパターンのみを例外とし、②のケースを例外として表現してはならない、というのがポイントになります。
この説明だけだとまだ業務エラーと例外の違いがわかりにくいので、もう少し突っ込んで説明をしてみます。
今、このアプリケーションで③に相当するケース、すなわちデータベース接続エラーやネットワークエラーが発生したとします。通常、これらのエラーはエンドユーザに対して通知するべき内容ではありません。というか、「データベースに接続できませんでした」、とかいわれても、エンドユーザ側としてはどないせいっちゅーねん、状態になりますよね?^^ おそらくこのような場合には、エンドユーザに対しては「ごめんなさい、またしばらくしたらアクセスしてください」「ヘルプデスクに電話してください」などのメッセージを通知するようにし、エラーの詳細情報は隠ぺいすることになるでしょう。
ところが、ID 重複エラーのようなものは、エンドユーザ側で対処のしようのある状況です。このような場合には、エンドユーザに対して適切なガイダンスメッセージを表示し、再入力などを求めることになるでしょう。
このように考えてみると分かりやすいと思いますが、
- .NET における「例外」とは、エンドユーザに対して通知すべきではない、「システム的・アプリ的な異常事態」が発生した場合に利用するもの。(上図の下側のような画面を出さなければならないケースは例外、ということです。)
- それ以外の場合、すなわちエンドユーザに対して再試行を促すようなメッセージを表示することになるケースを業務エラーと呼ぶ。業務エラーでは、例外を利用してはいけない。
というのが .NET 開発における原則、ということになります。
[2 種類の業務エラー]
なお、エンドユーザに対して再試行を促すようなメッセージを表示する業務エラーというのも、大別すると以下の 2 種類にわけることができます。すなわち、
- 生年月日テキストボックスから入力された文字列が、適切な日付でなかった場合にはエラーメッセージを表示する。(有効な日付であるか否か、また未来の日付でないか否か)
→ これは UI 部のみで完遂することが可能な入力チェックであり、単体入力チェックと呼ばれる。
→ 単体入力チェックは、UI (*.aspx)上でチェックをしてしまう。具体的には、ASP.NET 検証コントロールなどを利用してチェックを行う。
→ このため、ビジネスロジッククラス(BC)側は、単体入力チェックエラーとなっているようなデータ(例えば"2079年15月41日"などといったあり得ないデータ)を受け取ることはありません。 - 登録しようと思った顧客 ID が、他のユーザによってすでに使われていて重複していたためにエラーメッセージを表示する。
→ これは UI 部のみでは完遂できないチェックであり、複合チェックなどと呼ばれる。(※ 正式な呼び方はたぶんないと思いますので、仮にこのように呼ぶことにしておきます。)
→ このようなデータについては、ビジネスロジッククラス(BC)が受け取り、エラーであると判定された場合には、UI 部に戻り値として通知し、エラーメッセージを表示しなければならない。
→ つまり、UI 部のみではエラー処理を完遂できません。
となります。このことからわかるように、実は「何が業務エラーなのか」は、コンポーネント種別によってもかわってきます。上記の例でいうと、ビジネスロジッククラス(BC)部では、
- 単体入力チェックは UI 部で完遂しているはずなので、このパターンの業務エラーは考えなくてよい。
- BC 部では、データベース上のデータとの突き合わせなどを必要とする業務エラーについてのみ考えればよい。
となります。
[具体的なシグネチャ設計例]
ではここまでの話を元に、具体的に、このアプリケーションにおいてビジネスロジッククラス(BC)のメソッドシグネチャ(入力パラメータとメソッド戻り値)をどのような形にすればよいのかを考えてみましょう。
ビジネスロジッククラスでは、メソッド入力値として、希望する ID や名前、メールアドレス、生年月日などを受け取って処理を行いますが、その終了パターンは、① 成功、② 顧客 ID 重複、の 2 通りあります。これらを戻り値として表現できなければなりません。そこで、enum 値(列挙型)を利用すると、以下のようにメソッド定義をすることができます。
public class CustomerBizLogic {
public RegistCustomerResult ResistCustomer(string id, string name, string mail,
DateTime birthday) {
// ...
}
public enum RegistCustomerResult {
Success,
DuplicateCustomerIDError
}
}
ここで、メソッド戻り値としては、データベース接続エラーやネットワークエラー、あるいはメモリ不足などのケースは全く出てこないことに気を付けてください。こうした状況が発生した場合には例外が発生し、呼び出し元に例外として通知されるため、メソッド戻り値としては表現する必要がないからです。
今度は、このビジネスロジッククラスを利用する UI 部(*.aspx ファイル)の実装がどのようになるのかを考えてみます。業務エラーとは、エンドユーザに適切に通知する必要のあるエラーケースなので、正しくケース分類して、後処理を書かなければなりません。よって、UI 部のコードは次のようになります。
protected void btnRegist_Click(object sender, EventArgs e) {
if (Page.IsValid == false) return;
CustomerBizLogic biz = new CustomerBizLogic();
CustomerBizLogic.RegistCustomerResult result = biz.ResistCustomer(tbxId.Text,
tbxName.Text, tbxMail.Text, DateTime.Parse(tbxBirthday.Text));
switch (result) {
case CustomerBizLogic.RegistCustomerResult.Success:
lblResult.Text = "正しく顧客登録を行いました。";
break;
case CustomerBizLogic.RegistCustomerResult.DuplicateCustomerIDError:
lblResult.Text = "指定された ID はすでに利用されています。";
break;
}
}
基本的に UI 部では、BC からのメソッド戻り値を switch 文によって寄り分けて、その後の処理を書く、と考えると分かりやすいでしょう。(=try-catch などが出てくることはない、ということになります。詳しくは後述。)
[業務エラーが複雑なパターンとなる場合]
なお、上述の例では業務エラーが比較的単純なパターンのみを想定したため、メソッド戻り値としては「どの業務エラーになったのか」だけを表現すれば十分(=enum 型を使えば十分)でした。しかし、場合によってはサーバ側から、各業務エラーごとに異なる付帯情報を返したいこともあります。(例えば、顧客 ID が重複した場合には、サーバ側からおすすめ ID 情報をくっつけて返す、など) このような場合には、メソッド戻り値として構造体を利用するようなこともあります。
ちなみに、ビジネスロジッククラスのメソッド戻り値を使った業務エラーの表現方法は、おおまかに以下の 3 パターンにわけることができます。
もう少し具体的に書くと、以下の通りです。
① null 型
正常終了の場合には、通常の戻り値(オブジェクトインスタンス)を返し、業務エラーの場合には、null値を返す、という方式。
public AuthorsDataSet GetDataByAuId(string au_id);
→ 正常終了:オブジェクトインスタンスを返す
→ 業務エラー(著者IDが見当たらない):nullを返す
public decimal? GetPriceByTitleId(string title_id);
→ 正常終了:価格データを返す
→ 業務エラー(書籍IDが見当たらない):nullを返す
※ この方式、個人的にはあんまり好きではありません。というのも、"null" の意味付けが一定しなくなり、後からこのメソッドを読んだときに、null が何を意味するのかが曖昧になりやすいからです。
※ また、上記の例では、著者 ID が見つからなかった場合には、null を返す方式と、中身の行数がゼロ行の AuthorsDataSet インスタンスを返す方式が考えられます。実装上は後者の方がラク。どちらを使うにせよ、どの方式を採用しているのかは明確に仕様として管理しておかないと、実装ブレや後からのメンテナンス時のトラブルの元になりますので要注意。
② enum 型
成功/失敗などの情報のみを返せばよい場合には、各ケースを列挙型(enum)で表現して返します。(業務エラーのパターンが一つしかない場合には、bool型を利用しても OK。)
public OrderResult OrderBook(string customerId, …);
(OrderResultは列挙型)
→ 正常終了:OrderResult.Successを返す
→ 業務エラー(書籍の在庫がない):
OrderResult.NoStockを返す
→ 業務エラー(納品が間に合わない):
OrderResult.CannotDeliveryUntilRequiredDate
→ …
個人的には、コードの可読性が高まるので非常に好きな方式。呼び出し元でも switch 構文で後処理がきれいに書けるというメリットがあります。
なお、絶対にやってはいけないのは、数値コードを使って戻り値を表現する方法。例えば上記の例の場合、int 型を戻り値にしておいて、0 だったら正常終了、1 だったら在庫不足、2 だったら納期が間に合わない、などとするのは NG。これをしてしまうと、数値コード対応表がないと、アプリケーションプログラムの意味がわからなくなってしまうためです。アプリケーションプログラミングの大原則の一つに「Self-Descriptive(自己記述的)」(アプリケーションコード自体が、その意味を表現できていなければならない)という原則がありますが、その原則を守るためには、数値コードは使うべきではありません。
③ 構造体クラス型
正常終了/業務エラーなどの各ケースにおいて何かしらの付随情報を返す必要がある場合には、付随情報などをラッピングしたクラスを作成し、これを用いて戻り値を表現します。
public UpdateResult UpdateAuthorInfo(AuthorsDataSet ads)
{
...
}
[Serializable]
public class UpdateResult
{
public bool IsSuccess; // 正常終了 or 業務エラー
public UpdateErrorResultEnum BizError; // 業務エラーの理由
public int NewestCacheVersionNumber; // 業務エラー①の付帯情報
public AuthorsDataSet NewestInfoOnServer; // 業務エラー③の付帯情報
}
public enum UpdateErrorResultEnum
{
StateDataIsStale,
DuplicatePhoneNumber,
OptimisticCurrencyViolation
}
[例外の処理方法]
さて、ここまで業務エラーの設計方法と取り扱い方について説明してきました。ポイントとしては、業務エラーはメソッドシグネチャの一部として設計しなければならない、ということでしたが、では例外についてばとのように扱えばよいのでしょうか?
まず、大原則を覚えてください。
よほどのことがない限り、アプリケーションで try-catch を書いてはいけません。
もう一度繰り返します。
よほどのことがない限り、アプリケーションで try-catch を書いてはいけません。
もう一度(ry、すみませんしつこいですね^^。でもこれ、めちゃめちゃ重要なのです。
C# や VB を学習すると、割と早い段階で try-catch 構文による例外処理方法を学習すると思います。が、実際のアプリケーションコードでは、特殊な事情(後で説明します)がない限りは、try-catch を記述してはいけないのです。では try-catch を書かずにどうやって例外の後処理をすればよいのか....というと、通常、集約例外ハンドラと呼ばれる機能を利用します。
まず、例外処理(try-catch)や集約例外ハンドラを一切記述しなかった場合にどのような挙動をするのかを考えてみます。例えば、UI / ビジネスロジッククラス(BC) / データアクセスクラス(DAC)の論理 3 階層型構造で作成したアプリケーションを取り上げてみましょう。
この場合、業務エラーと例外は、次のように処理されます。
- 業務エラーが発生した場合
この場合は、例外を使わず、戻り値を使って上位コンポーネントに業務エラー発生を通知します。そして、Web ページではこの戻り値をガイダンスメッセージとして画面に表示し、ユーザに通知します。
- 例外が発生した場合
この場合、例外はどんどん上位の呼び出し元に通知されていきます。基本的に try-catch は記述してはならないので、例外は Web ページまで通知されますが、ここでも try-catch されていないため、例外は最終的には、実行ランタイムである ASP.NET ランタイムが捕捉します。すると、ASP.NET は、下図のような黄色い画面を表示します。
ちなみにランタイムが捕捉した場合の挙動は、アプリケーション種別ごとに異なります。
- ASP.NET ランタイムの場合 → 上図のような黄色い画面を出す。
- Windows フォームやコンソールアプリケーションの場合 → 下図のようなダイアログを出す。
このような画面は、開発時にはデバッグがすぐにできるので便利でしょうが、運用時には不適切です(運用時には、「しばらく時間をおいてやりなおしてください」とか「ヘルプデスクに電話してください」といったメッセージを表示する必要があるため)。このため、ASP.NET ランタイムや Windows フォームなどでは、これらの画面を差し替えるための機能を備えています。詳細はまた後で詳しく解説しますので、ここでは概要だけ説明しますと。
① ASP.NET ランタイムの場合
未処理例外が発生した場合には、global.asax ファイル内の Application_Error() メソッドが自動で呼び出されます。また、web.config ファイルの <customErrors> セクションを利用すると、エラー時に表示する画面を差し替えることができます。
<configuration>
<system.web>
<customErrors defaultRedirect="error.htm" mode="RemoteOnly" />
</system.web>
</configuration>
② Windows フォームの場合
未処理例外が発生した場合には、 Application.ThreadException イベントハンドラが自動で呼び出されます。よって、ここにエラーメッセージ通知を行うコードを記述しておくと good。最も簡単なサンプルコードを示すと以下のようになります。
namespace WindowsFormsApplication1
{
static class Program
{
[STAThread]
static void Main()
{
Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
{
MessageBox.Show("致命的な例外が発生しました。\nヘルプデスクに電話してください。");
Application.Exit();
}
}
}
③ コンソールアプリケーションの場合
未処理例外が発生した場合には、AppDomain.CurrentDomain.UnhandledException イベントハンドラが自動で呼び出されます。よって、ここにエラーメッセージ通知を行うコードを書きます。
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
}
static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
MessageBox.Show("致命的な例外が発生しました。\nヘルプデスクに電話してください。");
Environment.Exit(-1);
}
}
}
よって、これらの処理をカスタマイズしておけば、異常事態に対する後処理を個別に作り込む必要はない、ということになります。簡単にいえば、try-catch による例外の後処理を個別に作り込む必要性はない、ということです。
# 実際、私もいくつかの開発現場で、アプリケーションコード中に書かれた try-catch 文を片っぱしから除去してもらったことがあります。それぐらい、初学者は「なんとなく try-catch」を書いてしまうことが多いんですよね。この点は初学者が陥りがちな典型的なミスの一つなので注意してください。
[業務フローチャートと throw / try-catch の関係]
さて、ここまで try-catch 文は書くな、と解説してきたのですが、実際の業務アプリケーションの場合には、try-catch 文や throw 文を記述することもあります。どのような場合にこれらを記述する必要があるのかというと、それは業務フローチャートの流れの調整を行いたい場合です。もう一度、最初の方に掲載した図を再掲します。

この絵からわかるように、例外とは業務フローチャートの想定から外れた場合を表現する目的で利用します。が、場合によっては以下のようなケースもあるはずです。
- 間違って業務フローチャートから出ちゃったんだけど、元に戻って処理を続けたい。
- 自ら業務フローチャートの外に出て、自爆したい。
このような場合には、try-catch 命令、あるいは throw 命令を利用します。
上記の説明だけだとわかりにくいと思いますので、具体例を取り上げてみましょう。
① try-catch 命令を利用して、業務フローチャートに戻るケース
例えば、前述の顧客新規登録業務における、ビジネスロジッククラス(BC)の実装を考えてみましょう。Visual Studio 2008 の開発では、データアクセスクラス(DAC)の実装にテーブルアダプタを利用しますが、テーブルアダプタを介して INSERT 命令を発行すると、重複顧客がいる場合には、INSERT 処理が PK 制約違反により失敗します。
このとき、DAC から BC へは INSERT 命令失敗により、SqlException 例外が通知されますが、このケースは BC の立場からすると、例外として取り扱うべき事象ではなく、業務エラーとして取り扱うべき事象です。このため、BC 上では、try-catch 命令によりこの例外を捕捉し、通常の戻り値に変換して UI に返すことになります。
※ 以下のコードにはまだ問題があるので、続きの説明を読んでください。
public RegistCustomerResult ResistCustomer(string id, string name, string mail,
DateTime birthday)
{
// テーブルアダプタを利用
CustomersTableAdapter ta = new CustomersTableAdapter();
try
{
// INSERT 命令を実施
ta.InsertCustomer(id, name, mail, birthday);
}
catch (SqlException sqle)
{
return RegistCustomerResult.DuplicateCustomerIDError;
}
return RegistCustomerResult.Success;
}
しかし、ここで留意しなければならないことがあります。それは、SqlException 例外は、PK 制約違反以外のケースでも起こりうる、ということです。下表は、SQL Server 内部で起こる代表的なエラーと、それに対する例外の発生有無の概要表なのですが、これからわかるように、SqlException 例外は、ディスク枯渇やテーブルデータ破損といった、致命的なトラブルによっても発生します。このような事象は業務エラーでは当然ありませんが、上記のコードではすべて「重複顧客登録業務エラー」として取り扱われてしまいます。(=本来業務エラーとすべきではない SqlException まで業務エラーに変換してしまっている)
このような場合に役立つのが、catch ブロック内に記述する「throw」(引数なし)という命令です。この命令を使うと、catch したあとでありながら、catch しなかったことにできます。
public RegistCustomerResult ResistCustomer(string id, string name, string mail,
DateTime birthday)
{
// テーブルアダプタを利用
CustomersTableAdapter ta = new CustomersTableAdapter();
try
{
// INSERT 命令を実施
ta.InsertCustomer(id, name, mail, birthday);
}
catch (SqlException sqle)
{
if (sqle.Number == 2627) { // PK 制約違反だった場合には後処理を行う
return RegistCustomerResult.DuplicateCustomerIDError;
}
else {
throw; // それ以外だった場合には catch しなかったことにする
}
}
return RegistCustomerResult.Success;
}
以上の話からわかるように、try-catch 命令を書く場合には、業務エラーなどに変換したくない例外を、間違って捕捉しないようにしなければなりません。具体的には、以下のようなコードは絶対に書いてはいけません。
- 複数行のコードをまとめて try-catch で囲む。
- 一般例外(Exception クラス)を catch する。
- catch したあと何もしない。
static void Main(string[] args)
{
try // 間違い① 複数行を try-catch で一気に囲む
{
int a = 0;
int b = 0;
int c = a + b;
Console.WriteLine(c);
// ...
}
catch (Exception e) // 間違い② あらゆる例外を捕捉してしまう
{
// 間違い③ 補足したあと何もしない
}
}
このようなコードを書いてはいけない理由は、try-catch する目的を考えてみていただければ明らかでしょう。
- 複数行のコードをまとめて try-catch で囲む。
→ 捕捉して業務エラーに変更したい例外以外の例外が、別の行から発生し、それを捕捉してしまう恐れがある。
- 一般例外(Exception クラス)を catch する。
→ 捕捉したい例外以外の例外をも捕捉してしまう恐れがある。
- catch したあと何もしない。
→ 例外を「なかったこと」にしてしまう。
よって、例外を try-catch する場合には、以下の大原則を守らなければなりません。
- try-catch ブロックは、例外が発生しうる『1 行』のみを囲む。
- 一般例外(Exception クラス)ではなく、特定の例外(SqlException など)のみを捕捉する。
- catch した後には、必ず後処理(業務エラーへの変換など)を記述する。
※ リソースの確実な解放のために記述する try-finally の記述ではこれらに反するコードを記述することがありますが、これはそもそも目的が異なるためです。これについてはまた別の機会に。
② throw 命令を使って自爆するケース
では次に、throw 命令について説明します。throw 命令とは、アプリケーション内部から例外を発生させる処理ですが、これは自ら業務フローチャートの外に出て、アプリケーションを止めようとする自爆処理であると言えます。
例えば以下のような例を考えてみます。先ほどの新規顧客登録業務において、ビジネスロジッククラスの部分で、UI (*.aspx)から引き渡された電子メールアドレスの文字列が、○○@○○.○○ といった電子メールアドレスのフォーマットになっていなかった場合を考えてみます。
通常、ブラウザから入力される電子メールアドレスは、まず UI (*.aspx)上に貼り付けられた検証コントロールによってチェックします。このため、アプリケーションが適切に実装されている限り、ビジネスロジッククラス(BC)が、フォーマットのおかしい電子メールアドレスを UI 部から受け取ることはあり得ません。もしそのような事態が発生したとすると、それは以下のような状況が想定されます。
- UI 実装者が作業ミスあるいは作業を怠って、検証コントロールを正しく使わなかった。
- アプリケーションになんらかの脆弱性があり、悪意のあるユーザがこれを使って、無理やり不正なパラメータを投入してきた。
これらはいずれもヤバい状況ですが、このようなヤバい状況であるにもかかわらずそのままアプリケーション処理を続行すると、どんな事態が発生するのか分からず、極めて危険です。このように、アプリケーション内部で異常事態であることが判明した場合には、throw 命令を使うことにより、アプリケーションを停止させます。
例えば、先のこちらのコードに対して、「異常事態の判定コード」を追加してみましょう。
public RegistCustomerResult ResistCustomer(string id, string name, string mail,
DateTime birthday)
{
// テーブルアダプタを利用
CustomersTableAdapter ta = new CustomersTableAdapter();
try
{
// INSERT 命令を実施
ta.InsertCustomer(id, name, mail, birthday);
}
catch (SqlException sqle)
{
if (sqle.Number == 2627) { // PK 制約違反だった場合には後処理を行う
return RegistCustomerResult.DuplicateCustomerIDError;
}
else {
throw; // それ以外だった場合には catch しなかったことにする
}
}
return RegistCustomerResult.Success;
}
このコードの場合には、以下のような「異常事態」の検知コードを追加することができます。
- BC 部で入力値に単体入力エラーが発見された場合には、明らかにおかしい。
- INSERT 処理を行ったはずなのに更新結果行数が 0 行の場合には、明らかにおかしい。
よって、このような異常事態チェックコードを前出のコードに追加すると以下のようになります。
1: public RegistCustomerResult ResistCustomer(string id, string name, string mail,
2: DateTime birthday)
3: {
4: if (Regex.IsMatch(id, "^[A-Z]{2}[0-9]{4}$") == false) throw new ArgumentException("id");
5: if (Regex.IsMatch(name, "^[\u0020-\u007e]{1,20}$" == false) throw new ArgumentException("name");
6: if (birthday >= DateTime.Now) throw new ArgumentException("birthday");
7:
8: // テーブルアダプタを利用
9: CustomersTableAdapter ta = new CustomersTableAdapter();
10: try
11: {
12: // INSERT 命令を実施
13: int affectedRows = ta.InsertCustomer(id, name, mail, birthday);
14: if (affectedRows != 1) throw new ApplicationException("INSERT 処理に失敗しました。");
15: }
16: catch (SqlException sqle)
17: {
18: if (sqle.Number == 2627) {
19: return RegistCustomerResult.DuplicateCustomerIDError;
20: }
21: else {
22: throw;
23: }
24: }
25: return RegistCustomerResult.Success;
26: }
27:
このような異常事態検知コードについては、以下の点に注意してください。
- このような異常状態検知コードがなくても、アプリケーションは正しく動作する。
そもそもこのような事態は「起こってはいけない」「起こるはずのない」ものです。このため、これらのコードはなくても本来は問題ありません。ですが、これを入れておくことにより、「万が一」の事態に備えることができる、ということになります。
- このような異常事態検知コードをどこまで書くかは、ケースバイケースで考える。
上記では、単体入力エラーや、更新結果行数チェックコードを追加しましたが、このような異常事態は「考えれば考えるほど」もっとたくさん出てきてしまいます。が、あまりやりすぎても作業効率が悪くなるばかり、ですので、現実にはどこかで線引きする必要があります。通常は、単体入力エラーに関する異常状態チェック、DB 更新といった重要処理に関する異常状態チェックのみ行えば十分でしょう。
- 異常事態が検知されたときに throw する例外は、極端にいえば何でもよい。
異常事態発生時に throw する例外を何にするか、ということに関しては様々な議論があり、ApplicationException にすべきとか、その派生クラスを定義して throw すべきとか、いろんなことが書かれています。が、ぶっちゃけトークをすれば & 極端なことを言えばどの例外クラスを使っても OK。というのは、ここで throw した例外は、誰かが try-catch するわけではなく、ランタイムに拾わせてアプリケーションを停止させる目的で使うものだからです。とはいえ、あまりにも実態と異なる例外クラスを使うのも不適切なので、通常は ArgumentException 例外(入力パラメータに不正が見つかった場合)や ApplicationException 例外(アプリケーションの一般例外)を使います。(ApplicationException 例外を使うな、という資料も結構見かけるのですが、実際問題としては、上記のような理由により、そんなに神経質になる必要性はないです。)
- 異常事態が検知されたときに throw する例外には、詳細な情報を入れておく。
ここで発生された例外は、最終的に集約例外ハンドラにひっかかり、イベントログなどにロギングされます。この情報は、事後的な障害解析に使われるため、ここにどれだけの詳細情報を含めておけるのかが実は非常に重要になります。例外ログの読み方は今後解説していきますが、例えば、どういった理由・どういった実際の値でトラブルが発生したのかの情報を、例外内部に情報として含めておくとよいです。
- これらの例外発生コードは、カバレッジ上、通すことが難しいコードになる。
単体機能テストや結合機能テストを行う際、よく「コードカバレッジを 100% にしろ」といった無茶なことが言われますが、ここに書いたような異常事態検知コードというのは、テスト上、非常に通しにくいパスになります。しかしコードカバレッジを 100% にするために、これらの通しにくいパスを無理矢理通したり、あるいは(削っても正常なときには支障がないからという理由で)これらのコードを削ってしまう、というのは、あまり賢いアプローチとはいえません。通常、こうした異常事態検知コードの妥当性は、目視によるコードレビューによって判断し、単体機能テストや結合機能テストでコードカバレッジ 100% を目指す、という非効率的なアプローチはしない方が適切です。
[まとめ]
というわけで例外処理についてひたすら解説してきましたが、ここまでのキーポイントをまとめると以下のようになります。
- 例外は業務エラーには利用せず、異常事態(アプリケーション/システムエラー)の表現にのみ利用する。
- 例外が発生した際の後処理は、ランタイムの後処理機能に任せるようにし、基本的には try-catch を書かない。
- try-catch や throw 命令は、フローチャートの流れを調整したい場合に利用する。
上記 3 つの原則を守るだけで、アプリケーションの障害解析は圧倒的にラクになるので、まずはこれらの原則をしっかり守ったコーディングを行ってください。次回は引き続き、ASP.NET ランタイムの持つ 3 つの例外処理機能の使い方などを解説していきます。(と思いましたが、ちょっと回り道するかもしれません....。)