お疲れ様です。堺です。
プログラミング初心者の内は特にtry-catchは使いどころを間違えやすいですし、throw句で何故あえてクラッシュさせるの?とかも思いますよね。
また、熟練者に差し掛かっても、これら論点の運用はなかなか腹落ちしないものです。
私自身、10年以上独習でプログラミングを続けてきて、ようやくそれらに関して答えの一つが見えてきましたので、共有してみたいと思います。
◇目次
この記事が言いたいことまとめ
始めに結論をまとめてみます。
- 例外は、設計ミス等を未然検知し、また損害範囲縮小のため、あえてスローされる必要がある。
- ただし、コーディング規約、クラッシュした時の影響などは事前に確認する必要がある。
- try-catch句は、特定の目的をもって使用されるべき。初心者の内は特に不適切に使ってしまわないよう要注意。
詳細は次のセクションから述べていきます。
※本記事は設計思想的な面を含みますので、絶対的な答えではないことはご注意ください。また、今回はコード例ではC#を使用しますが、大半の言語で通用する話だと思います。
設計ミスに対しての例外スローは、ミスの被害を最小限に食い止める
リリースするプログラムでは様々なケースのテストを行うのは当然ですが、それでもミスを埋め込んでしまう可能性はゼロにはできません。
仮にそのミスに起因して誤計算などを起こし、しかも例外を投げずに表面上は正常に動いていたとしたら、プログラムの種類によっては後々大問題を引き起こす可能性があります。
そういった利用者のビジネス上の問題に発展する懸念に比べれば、その場で例外を投げてプログラムをクラッシュさせてしまう方が遥かにマシであるため、あえて例外をスローするということは十分にあり得ます。(もちろんプログラムの種類にも依ります。)
具体例で考えてみる
ごく簡単な例で考えてみます。
C#
var list = GetList();
if (list == null || list.Count != 1) {
// 例外をスロー
throw new Exception("リストの構成が想定外です!");
}
このケースでは、GetListメソッドから返却されたリストコンテンツが想定通りか否かを検証しています。(例えばSQLでSELECT文の結果を得るケースなどでしょうか。)
一意の列に対してサーチしているのに結果が2行以上存在しているなどの不具合は、致命的な計算ミスに繋がりかねません。よって、その場で例外をスローして処理を中断しています。
例外のスローは安全装置であり、開発チームに対する優しさである
この様に、例外のスローはバグを即座に顕在化させ、ビジネス上の損害を最小限に食い止める効果があります。
また、開発やテストフェーズにおいてもバグの取りこぼしの可能性を低めることが可能です。
開発チームメンバーが犯すミスが予見されれば、そこに例外処理を埋め込んでおくのは優しさ・思いやりであるかもしれません。
Index out of rangeといったような言語仕様に起因する例外だけではなく、開発者が明示的に例外処理を記述するのは、こういった背景があることを理解した上でコーディングを進める必要があります。
例外をスローするときの注意点:例外スローは、余計な情報(ソースコード等)が表に出ないように。規約も確認。
Webアプリの場合など特に、設定と環境によっては、例外が発生した際、ブラウザー上にソースコードが丸見えになってしまう場合があります。
ロジック内からの例外スローを許可する場合は、上位層でどう処理されるかをキチンと把握する必要があります。
また、チームで開発している場合は当然ながらプロマネ等に確認を取り方針を確認するようにしましょう。
例外をキャッチするtry-catch句の使いどころは?
この様に、例外は必要に応じて投げる必要があることがわかりました。
一方、try-catch句で例外をキャッチ(捕捉)する方が良いケースを検討します。
必要なエラーが隠蔽されてしまう可能性があるので無闇な利用は避けるべきですが、例えば以下のようなケースでは逆にtry-catch句を使用することが必要と思われます。
いずれにしても、使用する目的をはっきりさせた上で組み込むことが望ましいと言えます。
呼び出し元(上層)でまとめて例外を捕捉する場合
メソッド内でスローされた例外は、呼び出し元に連鎖して伝播し、最上位層で捕捉されなければプログラムがクラッシュします。
上述の通り、実際にクラッシュさせると利用者に余計な情報を与えてしまい、脆弱性や情報流出に繋がるケースも考えられます。
これを防ぐため、処理のエントリポイントでまとめてキャッチしてしまうのも一考です。
設計的に正しいフローでも例外が出る可能性が明らかであり、他に事前の検証手段がない場合
例外が発生しないことを事前に検証する手段がない、あるいは処理コストが高くなる等の場合、あえてtry-catch構文を使用することは考えられます。
代表的なものとして、次の外部リソースの利用が挙げられます。
外部リソース(例えばWeb APIやDB)へのアクセスなど、コード内からマネージ不能な呼び出しに対するフェール・セーフとして組み込む
外部リソースとの連携では、失敗することもあるという想定も含めて設計する必要があり、フェール・セーフ(fail safe/失敗時に安全に制御すること)を組み込むことが求められます。
一般に成否を事前検証する手段はなく、純正APIは失敗すると例外を投げてくることが多いので、安全のためcatch句で処理します。
C#
public DataTable? GetFromDatabase()
try {
//データベースに接続してデータをロードする処理と想定。
//接続はDBサーバーの状態など様々な要因で失敗することがあります。
return Load();
} catch {
//何らかの要因でDB接続できない場合でも例外を投げずに、
//その結果を返すことでユーザーにリトライを求める処理につなげ、安全にアプリケーションを続行します。
return null;
}
}
NGなtry-catchの書き方
初心者あるあるなtry-catchのNG例を考えてみます。
検証用途で使ってしまう
try-catchは例外のキャッチを行う構文ですので、その他の検証用途で使用するのは基本的には避けるべきです。
また、上記のように必要なエラーを隠蔽してしまわないように注意しましょう。
極端な例になりますが、try-catchを使用すべきでない場面だけ例示してみます。
C#
// NGと思われるコード
// (listの要素数検証はtry-catchで処理せず、極力事前検証するべき)
public int? GetFirst(List<int> list)
try {
return list[0];
} catch {
Console.Write("エラー:リストが空だよ");
return null;
}
}
//修正後のコード
public int? GetFirst(List<int> list)
// 事前検証
if (list.Count == 0) {
return null;
}
return list[0];
}
処理全体をtry句にネストしてしまう
プロマネがクラッシュが絶対ダメと言ってるから、処理全体をtryに入れてしまえばいいんだ!というもの。
気持ちはわかります。
ただ、不具合の理由が分からなくなりますし、しかも想定外の例外までキャッチしてしまうため、かなり好ましくありません。内部で例外が出るようであれば、無闇にキャッチせずにコードを修正するべきです。
C#
public void Main() {
try {
//いろいろな処理
//・
//・
//・
} catch {
//何もしない
}
}
結び:例外とtry-catchの運用は、成果物の品質に直結する重要論点
改めて、品質に直結する論点だと思いますので、プロマネ、コーダーを問わずよく検討した上で方向性を決めるべき議題だと思います。
是非この記事もご参考いただいた上、よりよいプロジェクトにしていただければと思います。
お目通しいただき有難うございました。
記事筆者へのお問い合わせ、仕事のご依頼
当社では、IT活用をはじめ、業務効率化やM&A、管理会計など幅広い分野でコンサルティング事業・IT開発事業を行っております。
この記事をご覧になり、もし相談してみたい点などがあれば、ぜひ問い合わせフォームまでご連絡ください。
皆様のご投稿をお待ちしております。