第12話 例外は例外だから例外じゃないの?
タイトルを見たらだいたい何のことかは察しが付くと思いますが、今回はそんな話。初めて書くエンジニアらしい話題かも。
■例外の違和感
わたしは、前にも少し書きましたが、エンジニアとしては「ハードウェア設計→アセンブラ→PL/I→C言語→VC++→Java→.NET(VC#)」……こんな感じで(途中、細かい物は端折ってますが)来ています。普通のソフトウェアエンジニアの人と違うのは、ハードウェア設計が始まりという点でしょうか。なんだかいい感じにソフトウェアの変遷の歴史みたいです。
最近は公私ともにVC#が多いです。動かす分にはかなり簡単にコーディングできますし、.NETもハードウェアの高速・大容量化に伴い非常に実用的なレベルになっています。仕事ではVC#とJavaが今は多いです。
ただ一点、どうしてもなじめない部分があります。それは、
例外処理が多くね?
と感じる点です。Javaと.NETでは若干、例外の考え方が違うのでひとくくりにはできませんが、何にしても多いと感じます。C++でもありますが、あまりコーディングに埋め込んだ記憶はありません(それはそれでいいのか、とも思う)。
確かにJavaやVC#は言語と実行環境がセットになっているので、そういう意味では言語仕様がそうなっていても不思議じゃないのですが、やはりわたしは違和感を感じます。それはナゼかといえば……
例外は本来、起こってはいけないこと(のはず)だから、きちんと設計して例外が出ないようにすべきではないのか?
と考えているからです。うーん、思い切り古い考えでしょうか? まぁ、VC#の場合はこう言い切ろうと思えばできないこともないと思うのですが(あくまでも言語仕様として、ですが)、Javaの場合はIOやデータベースアクセスなどはthrows/try~catchが必須(検査例外)なので、イヤでも例外処理が必要となります。
現場で実際に使っている人は分かっていると思いますが、この例外処理のおかげで意外と「止まらない」ソフトのコーディングが容易になっています。アセンブラやシステムソフトでスーパバイザモードで動くソフトのプログラミングをしてきた人間からすれば、いきなりどこかへ暴走してしまうより全然良いのですが(比べる対象が違いすぎ)、そういうプログラミングをしてきた(古い)エンジニアは、やはり「何なんだ」と思ってしまいます。まぁ、実際は例外という名のエラー処理機構なんでしょうけど。例外っていう名前が大げさなだけかも知れません。
■例外について考えてみる
Cの場合、例外処理は言語仕様として存在しません(C++はあります)。Javaや.NETから習い始めれば例外処理は当たり前なのかも知れませんが、アセンブラやCから始めていくとtry~catchにはやや違和感があります。わたしだけかも知れませんが。
Cの場合、例えばIOで何か起こった場合、ポインタ(ハンドル)はnullが返ってくることが多いので、例外で止まるということはありません(違う理由で止まることはあり得ますけど)。それに、変数も型はありますが、所詮中身はバイトコードなので、文字だろうが数字だろうが自分の想定した結果にならないだけでお構いなしです(それはそれでいいのか?)。
そもそも例外のイメージって、メモリアクセス違反とゼロ除算ぐらいしかパッと思い浮かばないのですが、突き詰めてしまえば「例外」=「エラー」なので、どうしても「例外が発生しないようにすべき」と考えてしまいがちなんです。
■では、いかに例外をつかうか
Javaと.NETでは少しだけ例外の扱いに違いがあります。知っていると思いますが、Javaは基本的にエラーを例外で返すことを推奨しています。.NETは業務エラーに関しては例外ではなく戻り値で返して、システム/アプリケーションエラーは例外を使いましょう(ちょっとニュアンスが違うかも知れませんが)とあります。
優秀なエンジニアはこの違いを解釈してコーディングします。ダメなエンジニアは分かっているつもりで適当にコーディングします。ちなみに、スキルの低い人ほど設計思想が違うからといって、なぜかおかしなコーディングします(後述)。
アンチパターンを言い出すと、これも宗教論争に発展しそうなので、わたし個人の方針としては.NETの業務エラーのみ戻り値で行うことと、例外をパラメータチェックに利用しないことだけを徹底させます。
基本方針は、やはり例外がなるべく出ないような設計、コーディングすべきということです(しつこいですか?)。ただし、I/Oやデータベースアクセスなどは相手がどうなるかまでこちらで関知できないので、そういう部分はちゃんと例外として処理させるべきでしょう。安易な例外処理はかえってシステムの動作をおかしくする危険性をはらんでいます。また、例外発生時にきちんとした処理をしないと、障害発生時の原因究明に支障が出ます(あたりまえだけど、意外と分かっていない人が多い)。
Javaや.NETの例外クラスは感心するぐらい系統化されていて、ちゃんと設計すれば何が起きても取りあえず処理の継続は可能ですし、原因追及も意外と容易になります。特に、Cのようにポインタを比較してnullなら……なんてことをしなくても、try~catchで実装しておけば例外がスローされた時点ですぐに例外処理ができるところなんかは、便利でいいなぁとつくづく思います。
ただ、わたしが感じるのは、「try~catch」を「例外が発生してもエラーを出させないための機構」と勘違いしているエンジニアが意外と多い、ということです。
■業務エラーを考える
Javaの場合、エラーを例外で処理することにより、表に出てくる戻り値はすべて正常な処理になるので、想像するよりもきれいなコードを書くことができます(あくまでも主観)。まぁ、どんな風に処理しても、センスのある人ならきれいにコードを書くし、なければ汚いコードになるのですが。
.NETの場合は業務エラーは戻り値で、とあります。ようするに、ビジネスロジックとしてのエラーはメソッドの戻り値でやれや、ということなのですが、時々この切り分けが難しいケースがあります。
try
{
num = Int32.Parse(入力データ); // VC#
// num = Integer.parseInt(入力データ); こっちはJava
}
catch(Exception ex)
{
// 例外発生時(入力データが半角数字以外の時)の処理
}
ここで問題になるのが「入力データ」です。フォームから入力したデータが文字列だったら明確な「業務エラー」だといえます。しかし、ファイルから読み込んだデータだった場合、通常は想定外の文字列が入る訳はないので、エラーになった場合は業務エラーではないといえます(設計上、ファイルから読み込むデータが半角数字以外も想定される場合は、上記のようなコードを書くのが間違っています。ただし、読み込んだ後に半角数字だったら、という場合はありですが、今回はそこまで深読みしません)。
先ほど、「例外をパラメータチェックに利用しないこと」と書きましたが、入力データがフォームからの入力だった場合はtry~catchで囲まずに、事前にバリデーションなどでチェックしておくことが正解です。そうすればおのずと「業務データ」は例外で処理しなくても済みます。心配性の人はチェックした上に上記のようなコーディングをします。理由を聞くと「万が一、何かあったら……」といいますが、万が一を考える前にしっかりテストする方が重要じゃないの、とわたしは思います。この考えがテストをおろそかにする第一歩です。
■これはあかんだろう
わたしが見てきた中で間違っていると思われる例外処理の仕方を少し紹介します。実話なので取りあえず笑って反面教師にして下さい。
1. 例外握りつぶし(ASP.NET/VC#)
Webアプリケーションの案件の場合、顧客から「エラーが出ても画面にエラーを表示して止めないで、処理を続行してください」という要求をされることがあると思います。わたしも当然、言われたことはあります。ただ、この言葉だけを鵜呑みにしたケースを見て「何だ、これは!」と思わず叫びそうになったことがありました。思い切り火を噴いていたプロジェクトに入ったのですが、コードを見ると例外処理に限らず、あちこちのコードが適当に動くように書いてあるので、途中で修正をあきらめました。
だいたい、察しは付くと思います。こんなコーディングしているメンバーがいたら早めに教育しましょう。無理だったら外しましょう……。
try
{
// 何らかの処理
}
catch
{
}
まず、catchに何も記述していないので(こういうことができない仕様にすれば良いと思うんだけど)、処理で例外が発生すると思い切りスルーします。もう何があってもお構いなし。例えばデータベースから条件が合うデータを読み込んで表示する場合、画面に1件も表示されなければそれは本当に1件も引っかからなかったのか、それとも例外が発生して表示されていないのか分かりません。しかもログに記録している訳でもないので、何が何だかわかりません。
本人に聞いてみると、「エラーを表示して止めるなと言われたので」と当たり前の顔をして言いましたが、例外処理のほぼ全部がこんなコーディングなので、動かしてみるともうむちゃくちゃでした。ちなみに不備を指摘したら逆ギレして「.NETはそういう設計思想なんです!」「お客様の利便性を考えたらこれが一番じゃないですか!」と言われて、どう言い返せば良いのか分かりませんでした。利便性も何も、そのお客様からクレームが一杯上がっていて、わたしが手伝いに入っているのに……おいおい。
2. 意味不明のフラグ(ASP.NET/VC#)
処理が正常かエラーかをフラグにセットして、それぞれ該当する画面に遷移させようとする処理だけど、本当に例外処理を分かっているよね? って疑うコーディングです。フラグにするか例外にするか、どっちかにしたらって感じです。
bool flag = true; // 処理判定フラグ
try
{
// 何らかの処理
//
if (何らかの処理 == 正常)
{
flag = true;
}
else
{
flag = false;
}
}
catch (Exception ex)
{
// スタックトレースをログへ書き出す
}
//
// 処理
//
if (flag)
{
Response.Redirect("~/True.aspx"); // 正常終了時
}
else
{
Response.Redirect("~/False.aspx"); // エラー終了時
}
最初見た時は「ああ」ぐらいしか思いませんでしたが、よく見たら「えー!」というコーディングでした。実際、解説するのもアホらしいのですが、これは「何らかの処理」で例外が発生しても正常終了時の画面が表示されます。
こうなった理由を聞いてみると、初めはtry~catchなしでコーディングしていたら、テストで例外が発生したので急遽、例外処理を付け加えたとのこと。何も考えず取って付けた例外処理は例外ではなく論外です。
3. 不必要なthrows(Java)
ダメというよりは「良くないんじゃない?」という例かな。個人的には「ちゃんと理解してる?」と疑ってしまうコーディングですが、まぁほとんどの場合は保険も兼ねてコーディングしていると思います。わたしは反対ですが(笑)。
void read_data() throws FileNotFoundException, IOException;
「FileNotFoundException」は「IOException」のサブクラスなので、普通は以下のように書きます(よね? 少なくともわたしはこう書きます)。
void read_data() throws IOException;
「FileNotFoundException」を明示的にthrowsしたいのならば、考えられる他の「IOException」のサブクラスの例外と組み合わせて使うことが望ましいかな。でも、たくさん書くなら「IOException」でいいんじゃない、とわたしは思うんだけど。
4. 結構多いアンチパターン
目に付くのが「finallyブロック内でのreturn」です。どうしてかといえば、例外発生時に例外情報を捨ててしまうからです。文法的には間違いではありませんし、例外でなければ問題ないです。でも、例外発生時に何とかするために例外情報を使うのに、その情報を捨ててしまったらなんの意味もないです。ちなみに、個人的にはあまりreturnが多いコードの書き方は好きじゃないです。
あとはcatchのところが全部「catch(Exception ex)」になっているケースです。処理したいのは分かるけど、やっぱり発生する可能性のある例外毎にキャッチしましょう。手抜きは良くないです。
■なぜこうなるのか
教えてもらうにしても学ぶにしても、例外を後回しにしていませんか? 設計やコーディングを見ていると、ちゃんと考えて例外処理をしているな、と思えることは少ないです。例外を正しく考えて設計できる人にテスト仕様書を書かせると、それなりのクオリティのものを作ってくれます。それだけいろいろなことを想定できるからなんでしょう。特にJavaの検査例外は強制なので、開発環境やコンパイル時にエラーが出るために「仕方なく」付け加える人もいるのではないのでしょうか。スタックトレースがうざいっていうのは論外です。
動けばよし、ではダメで、例外処理(try~catch)を取って付けたようなコードは、逆におかしな処理を生み出します。必要なところにはちゃんと設計した例外処理を書き、不安だったらまず処理を見直すところから始めましょう。間違っても「念のため」とかやり出すと例外処理だらけになってしまい、本当に例外が発生したら収拾つかなくなります。
わたしは設計思想やコーディングの美しさは基本的に「どうでもいい派」です(でも、見やすくないと困るとは思いますが)。確かにJavaや.NETなどでいろいろ決まりはありますが、それよりは「エラーを正しく処理すること」と「例外が出ないようなコーディングを考えること」が一番だと思っています。思い込みだけで設計・コーディングしても、要求されたニーズを満たせるとは限りません。
お客様に言われても、ウラではちゃんと処理すべきですし(ログを出さないなんてもうダメを通り越して素人以下と言われても仕方ありません)、流れを止めてでも「エラー」にすべき時はちゃんと理由を明確にしてお客様と交渉すべきです。エラーをリカバリできるシステムなら問題ないですが(そういうシステムは作ったことがあるので)、そうでなければ確実に欠陥品ができあがります。言われたまま鵜呑みにしてシステムを作ることは、素人にだって今はできます。
次は「教育」をテーマに書こうと思ってます。最近、コラムのネタがいろいろな人と被っていますが(苦笑)。……それはそれで考えながら書こうと思います。わたしは、職業エンジニアには教育が必要だと考えています。子供と一緒で、何も教育しなかったらまともなエンジニアにならないとさえ思ってます。
次のコラムでまた一区切りにしたいと思います。
コメント
はやしさとし
あれ?どこかで聞いたような話だと思ったら…あああ!
思い出しました。
http://blogs.msdn.com/nakama/archive/2009/01/09/net-java.aspx
ついでにすいません。
サンプルコードへのつっこみです。
> if (何らかの処理 == 正常)
> {
> flag = true;
> }
> else
> {
> flag = false;
> }
8行も使用していて、ムズムズしてきました。
単純に1行で
flag = (何らかの処理 == 正常);
と書きたいです。
あえてシンプルなコードにした結果そうなったなら、ごめんなさい。
にゃん太郎
はやしさとしさん、ありがとうございます。
> あえてシンプルなコードにした結果そうなったなら、ごめんなさい。
分かりやすくしているだけです。コードの書き方を言うとカッコと同じような論
争になりそうですが、私のグループではこのコードを一行にする事は基本的に禁止
です。
例外の比較ってあんまりないなぁと思ったらあるんですね。Javaについては結構
ありますが、両方同時に面倒みているのでいろいろなケースをやらかしてくれま
す。個人的にはJavaの方が使いやすいかな、と思いますがどうせ似てるなら同じに
しておいてくれた方が良かったのに、と憤慨しています(八つ当たり)
saki1208
にゃん太郎さん、こんばんは。
saki1208です。
ウチでも昔はよくありましたよ。
書いてある通りに動かないと言う後輩のソースを見たら、各ブロックの先頭に
「On Error Resume Next」って書いてある...orz
# VB6以前/VB Scriptの時ですけどね。
例外処理って便利なんですけど、ちゃんと理解して書かないと最終的に一番外
側のcatchまで伝播されるので最初にどこで発生したかを掴めないロジックに
なってしまうんですよね。
# せめて、throwしてくれればいいんですが...
仰る通り、業務的な処理の内部でチェックすることによって防ぐことが可能な
エラーは事前にチェックしてから処理を進めるべきだと思います。
にゃん太郎
saki1208さん、ありがとうございます。
確かにthrowもうまく使えない人が(私のまわりには)多いです。そもそもスルー
するコードだと画面見てても気がつかないし。フォームから何度登録してもOKは出るのにDBに書き込まれていないので、調べたら例外が発生しているし。お客さんも
電話で「いやー、エラー出てないんだけどねぇ」とか言ってるけど、まさかこれが
原因とも言えず、ホントに笑えません。
あと、例で出したInt32.Parseですが、なぜあれかと言えば、あのコーディング
で入力エラーチェックしているのを見て気がつきました。実行速度とか処理が重い
とかいうんじゃなくて、エラーはちゃんとチェックしなさいといつも口を酸っぱく
して言っているのですが、なかなか治りません。
ビガー
ビガーです。
とりあえず、例外についてですが、Cやアセンブラでは「コンポーネント」という発想がないので、比較対象として微妙な部分があると思います。Cなどの構造化言語は戻り値で判断するしかなく、本来の戻り値に目的とマッチしないと考えます。
コンポーネントで起こりうる例外的状態がthrowsに記述されるべきという意味で、例で挙がっていたIOExceptionの話はその状況により、あるべきシグニチャは変わります。
また、コンポーネントを作るのか利用するのかを正しく理解することが重要で、利用する場合には例外を捕捉するべきなのか、伝播するべきなのかを適切に判断する必要があります。
>わたしは設計思想やコーディングの美しさは基本的に「どうでもいい派」です
私は、設計思想(言語仕様そのものではなく)については、プロジェクトの基準になると判断しているので重要と考えます。
言い訳としての設計思想というのは、論外ですが、どちらかというとアプローチが設計思想のカギを握っているのではないかと考えています。
データ中心指向であるのか(あるべきなのかを進言するかはプロジェクトの進捗具合で異なる)オブジェクト指向であるのか、まさかの処理中心なのか。
オブジェクト指向は、分析から実装まで意味を一貫して落とし込みやすい(要件定義フェーズの思想が実装へ反映されやすい)というのが良い点で、エンドユーザにとっても納得感が高いところが最大の利点です。このあたりのやり方は、私のコラムでおいおい。
コーディングについては、コードを美しくしたいという欲求は大事だと思いますが、ある程度経験を積むとOSSで提供されているものと似た構造になります。欲求はスキルアップに繋がるけど、コダワリはプロジェクトの害になる可能性が高いので控えるべきかなと思います。
にゃん太郎
ビガーさん、ありがとうございます。
> 例外を捕捉するべきなのか、伝播するべきなのかを適切に判断する必要があります。
saki1208さんも書いていらっしゃいますが、「捕捉」や「伝播」がなかなか適切
に判断できない人もいます。これに関しては経験もある程度は必要だと思います
が、いろいろなやり方が出来るので人によって差が出来てしまいます。うちの場合
は(例外に限らず)コーディング規約で結構縛るようにしてるのですが、それでも
メンバーによって好き勝手に解釈してしまうあたり、例外って奥が深いのかな、と
思います。
> 欲求はスキルアップに繋がるけど、コダワリはプロジェクトの害になる可能性が
> 高いので控えるべきかなと思います。
これは少し次のコラムでも書いてますが、仕事においては個々のこだわり(カッ
コの話のような)は基本的にさせないよう指導するようにしています。自分も含め
て。趣味や勉強会の時に検討する時などはおおいにこだわってもらえば良いと思い
ますが、仕事で書くコードは自分の物ではなくお客様の物だという事を常に意識す
るように言います。今はコンピューターのリソースも昔に比べて低価格で高性能
(高容量)なので、昔みたいにリソース不足をプログラミングでカバーする事もほ
ぼ不要(100%かと言われるとどうかな、と思いますが)でしょうし。
ちなみにここでの設計思想はコーディングにおいて、の事です。ただ、コーディ
ングに設計思想が関係するのかと言われれば個人的にはそこまでないと思いますが
(意識しようとしよまいとコーディングレベルになるとほぼ設計思想に沿った形に
なると思うので)今回のコラム中で出てきた逆ギレの部分をさして書きました(つ
まり、設計思想をたてに言い訳しない意味で「どうでもいい」です)ですから根本
の重要な部分についてはおっしゃる通りだと思います。コラムでおいおい書かれる
との事なので、それは楽しみにしています。
インドリ
おはようございます。
例外は奥が深いですよね。
ライブラリとUIでは例外処理の守るべき規約が違いますし、細かい事を言い出せばきりがありませんよね。
しかも例外処理を「例外的なイベント」の事と考えている人が多いようです。
例外処理の認識が個々に違うので困まります。
にゃん太郎さんが仰るようにコーティング規約でがっちり定義する必要があると思います。
業務エラーについてなのですが、私はシビアに考えますので「戻り値」ではなくて全て例外をスローするようにしています。
そして、ユーザーが簡単に報告できるようにしております。
あと、例外処理のコーティングルールも勿論提案します。
チーム全員がルールを守らないとプロジェクトに混乱をもたらします。
デバッグは計画的にですよね。
こういうコラムも中々面白いですね。
次回も楽しみにしています。
※ハードルをあげるために書いているわけではないですよ。
ocha
にゃん太郎さんはじめまして。
いつもコラムを楽しみにしております。
私は最近までずっとC、C++メインに使っており、
にゃん太郎さんと同じように例外はなるべく書かないようなコーディングをしてきました。
しかし仕事でRubyを使うことになり、例外処理の使い方にさんざん頭を悩ませましたが、
最近ではやっとRubyの例外処理を活用できるようになりました。
言語の設計思想が違うのだから、各言語の特性を理解したコーディングを心掛けたいですね。
もっと私の頭がさくっと切り替わってくれると助かるんですが・・・。
次のコラムも楽しみにしています。
pedestrian
FileNotFoundExceptionのくだりですが、
以前いた会社では結構たくさんの例外をthrowsに書いてました(基本的に派生元はすべて同じ例外クラス。javaの例外そのものの場合もあり)。
例外を元にエラー内容をダイアログでクライアントに通知する仕組みだったので
throwsには発生する例外すべてを書いておけという方針です。
APIとして他のチームが利用するという手前もあったかもしれませんが。
まあ、instanceofあたりで使う側がどんな例外が発生したかを判定するという手もありますが、throwsに具体的な例外を書いておくと使う側からすると分かりやすいというのはある気がします。
にゃん太郎
インドリさん、ありがとうございます。
> しかも例外処理を「例外的なイベント」の事と考えている人が多いようです。
その通りです。正直に白状すると、私も最初はそう思っていました。
例外って確かに奥が深くて良くできていると思います。ただ、指針みたいなものはあっても絶対ではないのでインドリさんのように戻り値ではなく例外をスローするのもありと思います。余談ですが、例外のスローもなかなか上手な人が少ないです。
出来るからでいい、ではなく出来てしまうからこそ安易にやって欲しくないですよね。ホントに計画的にだと思います。
にゃん太郎
ochaさん、ありがとうございます。
> もっと私の頭がさくっと切り替わってくれると助かるんですが・・・。
おっしゃる事は非常によく分かります。私に限らないと思うのですが、今までの経験から新しい処理方法を考えるとギャップがあって年々厳しいと感じてしまいます(この辺りがプログラマ35歳定年説だとおもうのですが)特にJavaやC#はC/C++と大差ないように見えるだけにやっかいです。同じ考えを持ち込むと「???」ですから。
これからも期待に添えるように書きますので、よろしくお願いします。
にゃん太郎
pedestrianさん、ありがとうございます。
> throwsには発生する例外すべてを書いておけという方針です。
同じ親クラスの例外をたくさん並べると見にくいと感じますが、状況によってはその方が良いのかもしれませんね。
たくさん並べるで思い出したのがSQLのSELECT文の「*」です(SELECT * FROM TBLなど)会社によって禁止したり許可したりがコーディング規約であるのですが、でも、たくさん項目名を並べるのがイヤだからか禁止の所でもコードを見ていくと「*」がついている事は結構あります。
throwsもそうですが、どちらでも出来るので管理する方とすれば結構悩ましい問題です。
組長
こんばんは。
開発のことは、とんと分からない私ではありますが、一点とても気になる一文が・・・。
>子供と一緒で、何も教育しなかったらまともなエンジニアにならないとさえ思ってます。
ジャンルを問わず、これってすごく正しいと思います。おそらく、にゃん太郎さんが想定しておられる方向性と私が感じた方向性はちょっと違うかもしれませんが、文章にすると同じく、こうなります。次回も楽しみにしております。
にゃん太郎
組長さん、ありがとうございます。
書き方的には少し大げさな気は自分でもするのですが、趣味と仕事は違いますしスキルとモラルも違うと考えています。開発に限って言えばプログラムを設計したり作ったりする事だけを教えてもそれはただ「開発が出来る」エンジニアであって、仕事が出来るプロとは違います。だから「教育」が必要だと思います。
あんまり書くとネタばれなんで(笑)
また、比較しながらでもコラムを読んで頂ければ幸いです。
useless
本題とは直接関係ないかもしれませんが,例外の戻り値とは違う存在意義について思うところを一つ。
例外はメソッド(Cなら関数)呼び出しの深さに依存しない例外処理を(容易に)実現するための概念的サポートを備えている点が優れているのだと思います。
実装コードというものは,同じことを実現するコードであっても,呼び出しの深さが違うということがままあります。が,こういう種類の例外はこのレベルで処理したい,というのはだいたい変わらなかったりします。
戻り値ベースのアプローチは,基本的に直上の階層での例外処理を前提にしており,呼び出しのネストの深さを超える例外処理を行おうとすると,戻り値の設計に細心の注意が必要になり,また詳細度の高い設計になるため変更にも弱いコードになりがちです。
私は,例外機構は,実装(ネスト)に依存しない例外処理設計を可能にする仕組みだと考えています。