forked from dlang/dlang.org
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy patherrors.dd
219 lines (176 loc) · 10.6 KB
/
errors.dd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
Ddoc
$(SPEC_S エラー処理,
$(BLOCKQUOTE Julius C'ster,
I came, I coded, I crashed.
)
どんなプログラムでも、エラーを扱う必要があります。エラーとは、
プログラムの通常動作の一環ではない、期待されない状態のことです。
よくあるエラー状態の例としては:
$(UL
$(LI メモリ不足)
$(LI ディスク領域不足)
$(LI 不正なファイル名)
$(LI 読み取り専用ファイルへ書き込もうとした)
$(LI 存在しないファイルを読み込もうとした)
$(LI サポートされていないシステムサービスを要求した)
)
<h2>エラー処理にまつわる問題</h2>
伝統的なCのエラー検知/報告の方法は、伝統でも何でもなくて、
関数ごとに異なる場当たり的なものです。以下のようなパターンがあります:
$(UL
$(LI NULLポインタを返す)
$(LI 0を返す)
$(LI 非0のエラーコードを返す)
$(LI グローバル変数errnoをチェックするよう要求する)
$(LI 前の呼び出しが失敗していたかどうかを、
次の関数呼び出しでチェックする)
)
可能性のあるエラーに対処するためには、関数呼び出しそれぞれに対して
面倒なエラー処理コードを追加しなければなりません。エラーが発生した場合は、
エラーから復帰し、何らかのユーザーフレンドリーな方法でユーザにそれを
通知しなければなりません。エラーがその場で処理できなければ、
もっと上位のルーチンへそのことを伝搬しなければなりません。
長い長いerrno値のリストは、適切なテキストに変換して表示する必要があります。
このような作業をするコード全てを書くことは、プロジェクトのなかで多くの時間を
費やしてしまいます。さらに、もし仮に新しいerrnoの値がランタイムシステムに
追加されたら、
古いコードは意味のあるエラーメッセージを表示できなくなります。
<p>
きちんとエラー処理をしたコードは、それでなければ綺麗でまとまった実装を、
雑然と散らかったものに変えてしまう傾向があります。
<p>
もっと悪いことに、よくエラー処理をしたコードというのはそれ自体がエラーを生みやすく、
プロジェクト中で一番テストされていない(それゆえバグの多い)部分となるため、
単純に省略されてしまいがちなのです。こうなると結局、何か予想されないエラーを
プログラムが扱いきれず、"死の青画面" に突入してしまうはめになります。
<p>
ささっと適当なコードで済ましてしまうような小プログラムには、
ややこしいエラー処理を書くコストを払うだけ価値がなく、そのようなユーティリティは、
刃ガードのついていないノコギリのような存在になっていまいます。
<p>
エラー処理に必要な哲学と方法論は、次の通りです:
$(UL
$(LI 標準化されている - 一貫して使用することで、より使いやすくなる)
$(LI プログラマがエラーチェックをさぼったとしても、
ある程度意味のある結果を出力する)
$(LI 古いコードを新しいコードを組み合わせるときに、新しいエラーを、
古いコードを書き換えずとも扱えるようにする)
$(LI 不注意でエラーが無視されることをなくす)
$(LI $(SINGLEQUOTE ささっと適当に) 書いユーティリティでも、
正しくエラー処理がなされるようにする)
$(LI エラー処理のコードを見やすく書ける)
)
<h2>D でのエラー処理の解決策</h2>
まず、エラーについてよく考えて、幾つかの仮定を置きましょう:
$(UL
$(LI エラーはプログラムの通常の実行フローにはない。エラーは
例外的で、期待されないものである。)
$(LI エラーは通常状態ではないので、エラー処理のコードは
さほどパフォーマンスを重視しなくてよい。)
$(LI 逆に通常の実行ロジック部分のパフォーマンスが重要である。)
$(LI 全てのエラーは何らかの方法で処理される。
明示的に書かれたエラー処理コードか、
あるいはシステムのデフォルト処理か。)
$(LI エラー発生部分のコードの方が、エラーから復帰する部分のコードよりも
エラーについてよく知っている。)
)
解決策は、エラーの報告には例外処理機構を用いることです。
全てのエラーは
仮想クラスErrorから派生したオブジェクトになります。Errorクラスは純粋仮想関数
toString を持ち、エラーを人間に読める形で説明した char[] を返します。
<p>
コードが "メモリ不足" のようなエラーを検知したとしましょう。すると、
"メモリ不足" というメッセージを持った Errorオブジェクトが投げられます。
関数呼び出しスタックは、Errorのハンドラが見つかるまで巻き戻されます。スタック巻き戻しの途中には
$(DDSUBLINK statement, TryStatement, finally ブロック)
が実行されます。
Errorのハンドラが見つかると、
そこから実行が再開されます。最後まで見つからなければ、
デフォルトのエラーハンドラがよびだされ、メッセージを表示してプログラムを終了します。
<p>
これは、先ほどあげた基準にあっているでしょうか?
<dl>
<dt> 標準化されている - 一貫して使用することで、より使いやすくなる
<dd> ここに上げた方法がD流で、
Dのランタイムライブラリや例で一貫して使われています。
<dt> プログラマがエラーチェックをさぼったとしても、
ある程度意味のある結果を出力する
<dd> エラーに対するcatchハンドラが存在しなければ、
プログラムはデフォルトのエラーハンドラによってキチンと終了し、
適切なエラーメッセージを出力します。
<dt> 古いコードを新しいコードを組み合わせるときに、新しいエラーを、
古いコードを書き換えずとも扱えるようにする
<dd> 古いコードは全てのエラーをcatchするよう書くことも、一部をcatchして
残りは上位関数へ伝えることもできます。どの場合でも、
エラー番号と文字列を相関させる必要はなく、
常に正しいメッセージが与えられます。
<dt> 不注意でエラーが無視されることをなくす
<dd> Error例外は、何とかして処理されます。
エラーを示すためにNULLポインタを返して、その次の行で NULL
ポインタを使ってしまう、というようなことはなくなります。
<dt> 'ささっと適当に'
書いたユーティリティでも正しくエラー処理がなされるようにする
<dd> Quick and dirty コードでは、
一切エラー処理を書いたりエラーチェックをしたり
する必要はありません。全てデフォルトで、エラーが起きれば
適切なメッセージを表示してからプログラムはきちんと終了します。
<dt> エラー処理のコードを見やすく書ける
<dd> try/catch/finally 文は、延々と続く if (error) goto errorhandler; 文より
ずっと見やすいです。
</dl>
エラーについて設けた仮定とつきあわせてみるとどうでしょう?
<dl>
<dt> エラーはプログラムの通常の実行フローにはない。エラーは
例外的で、期待されないものである。
<dd> D の例外処理はぴったりこれに当てはまります。
<dt> エラーは通常状態ではないので、エラー処理のコードは
さほどパフォーマンスを重視しなくてよい。
<dd> 例外処理のスタック巻き戻しは、比較的遅いプロセスです。
<dt> 逆に通常の実行ロジック部分のパフォーマンスが重要である。
<dd> 通常の実行フローでは全ての関数のエラーチェックをする必要はなくなるので、
エラー処理に対して例外を使うことで、
現実に高速化します。
<dt> 全てのエラーは何らかの方法で処理される。
明示的に書かれたエラー処理コードか、
あるいはシステムのデフォルト処理か。
<dd> もし特定のエラーに対してハンドラが存在しなければ、ランタイムライブラリの
デフォルトのハンドラで処理されます。仮にエラーが無視されるとすれば、
それはプログラマが明示的にエラーを無視するコードを書いたからで、
それは意図的な処理であると考えられます。
<dt> エラー発生部分のコードの方が、エラーから復帰する部分のコードよりも
エラーについてよく知っている。
<dd> もはや、エラーコードを人間に読める形式に変換する必要はありません。
正しい文字列は、エラー対処コードではなく、エラー検知コードから生成されます。
これはまた、同じエラーに対しては異なるアプリケーション間で一貫性のある
メッセージが表示されることにつながります。
</dl>
例外をエラー処理に用いることで、別の問題 -- いかに例外安全な
プログラムを書くか -- が持ち上がってきます。この問題については $(DPLLINK exception-safe.html, こちら) で。
$(COMMENT
$(OL
$(LI Programmers, especially inexperienced ones, tend to neglect
to test for the special error return value.
Their code just assumed the function completed successfully.
This leads to erratic and unpredictable
behavior if the function did fail.)
$(LI How each function deals with errors tends to be unique and
inconsistent, leading to more unintended
programmatic errors.)
$(LI How the error gets reported to the user tends to vary
arbitrarily from one program to the next and one
error case to the next.)
$(LI Dealing with error cases causes tedious and error-prone code
to be written, and so can consume much
programming effort.)
$(LI Error handling logic tends to be buggy because it rarely
gets tested by the test team.)
$(LI Functions that should have clean interfaces wind up
cluttering them with error return parameters and
cases.)
)
)
)
Macros:
TITLE=エラー処理
WIKI=Errors
CATEGORY_SPEC=$0