投稿者 魔界の仮面弁士  (社会人) 投稿日時 2018/2/5 19:09:02
> 理由としては、第46回クラス作成の中で
これのことですね。
http://rucio.a.la9.jp/main/dotnet/shokyu/standard46.htm


> 今回の場合はインスタンス化の前段階で例外が発生し、インスタンス化が無駄だと思ったからです。
であれば、例外オブジェクトのインスタンス生成も極力減らすべきかも…。

それはさておき、前提となる仕様が曖昧ですが、i / n の演算が失敗した場合には、
"0" という情報を出力する仕様ということでよろしいでしょうか。

今回の例だと、「前段階の例外チェック」と「ファイル生成の例外チェック」が
同じ Try 構文にまとめられているのが問題であるように感じました。
複数の Catch に分かれているとか、When や If などで分類されているわけでも無いですし。

異なる例外処理をひとつにまとめてしまうと、どの部分で問題が発生したのか
後で追跡しにくくなりますので、この点は見直した方が良いかと思います。

それに ゼロ除算の失敗というのは、事前に If 等で回避できるものなので、
この部分は例外に頼るようなものではないと思います。

そういった意味において、今回の例示では質問の意図が伝わり難いように感じました。


> 個人的には Write1() がいいかと思っています。
先述の通り、どちらも問題があると思ってはいるのですが、どちらかを選ぶとしたら
Write2 よりは Write1 の方が個人的には好みですね。

ファイルへの書き込みが失敗する理由は色々ありますが、今回のケースでは
 (a) ファイル名が間違っていて、StreamWriter を生成できなかった
 (b) ファイル名は正しいがアクセス権が無く、StreamWriter を生成できなかった
 (c) ファイルは正しく開けたが、ディスク容量不足で WriteLine に失敗した
 (d) 書き込むためのデータを計算する前処理に問題があった
などが考えられます。

Write1 であれば、a~d すべてに対処されていますが、
Write2 では a や b に対処できていません。

もちろん、Write1 のまとめ方にも問題があるのですが、もしも
Write2 のコードを採用したとすると、「Write2 を呼び出す側」でも、
a や b に対処するための Try が必要になってしまうので、
どちらかを選ぶとしたら、私は Write1 の方を選びます。
(「失敗の理由を気にする必要が無い場合」という前提条件は付きますが)


.NET の例外処理の考え方について、下記が参考になると思います。

【.NETの例外処理】
https://blogs.msdn.microsoft.com/nakama/2008/12/29/net-part-1/
https://blogs.msdn.microsoft.com/nakama/2009/01/02/net-part-2/
https://blogs.msdn.microsoft.com/nakama/2009/01/18/net-part-3/
https://blogs.msdn.microsoft.com/nakama/2009/01/23/net-part-4/

【エラーチェックの体系的な分類方法】
https://blogs.msdn.microsoft.com/nakama/2009/09/28/293/

【エラーチェックの体系的な分類と実装パターン】
https://blogs.msdn.microsoft.com/nakama/2009/09/28/303/


その上で、実際にどう実装するのかは案件次第ですが、たとえば、エラーの理由が

 (a) ファイル名が間違っていて、StreamWriter を生成できなかった
 (b) ファイル名は正しいがアクセス権が無く、StreamWriter を生成できなかった

であれば、失敗理由を伝えた上で、別のファイル名を再選択させるための
OpenFileDialog を再表示するなどの対処を行い、

 (c) ファイルは正しく開けたが、ディスク容量不足で WriteLine に失敗した

であれば、失敗理由を伝えた上で、ディスクの空き容量を確保させた上で
再チャレンジできるよう、 再試行/中止/キャンセル のダイアログを出すなどの
対処を行うといった実装を組み込むことができますね。

なお、
 (d) 書き込むためのデータを計算する前処理に問題があった
を例外で対処することの是非については先述の通りですが、
演算処理が複雑で事前チェックのコストが高すぎるようなケースでは、
これも別の例外処理として対処することがしばしばあります。ただその場合でも、
『Catch ex As Exception』で済ますのは、あまり望ましくないでしょうね。

あるいはユーザーへの問い合わせが行えない処理の場合(無人実行できる処理など)なら、
ダイアログで問い合わせてリトライさせるといったわけにもいきませんから、
問題の発生した処理のみをスキップして処理を継続するだとか、あるいは
例外を捕らえてエラーログとして記録するようにし、その上で、
一連の処理をロールバックするなどといった、要件ごとに個別の対処が必要ですね。

もしも例外情報を記録する場合は ex.Message では情報不足なので、
ex.ToString() を使ったり、例外を引き起こす要因となったパラメータも
一緒に記録するなどして、後からログの内容だけで情報を追跡できるように
記録することが望ましいです。

とはいえ実際には、そこまで詳細なログを生成するほどでもないというケースも
数多くありますが……それでも下記のような例外処理だと困ってしまいますね。
https://qiita.com/tokishirazu/items/b510ebfdb7ff089fe44c


以下蛇足:

> Dim i As Integer = 0
> Dim n As Integer = 0
> Dim j As Integer
> j = i / n
意図的に例外を発生させようとしているコードでしょうから
これについての説明は不要と思いますが、一応補足。


変数 i や n の値が変更されていないため、上記は
 Dim j As Integer = 0 / 0
に相当する処理になりますね。

「ゼロ÷ゼロ」の結果は未定義であるため、
「j = 0 / 0」と記述した場合はコンパイルエラー(BC30439)で失敗しますが、
「k = i / j」の場合は、値 0 だったときに OverflowException の例外が飛んできます。
j のみが 0 で i が 0 以外だった場合は、DivideByZeroException の例外ですね。

0 除算、特に 0÷0 に問題があることは自明なので、こういった処理は
Try~Catch で取り除くのではなく、If 文などで対処するのが一般的です。


ただしゼロ除算の件を抜きにしても、
> j = i / n
という処理は、まだ不自然な印象を受けます。n の値が 0 以外であったとしても。

「Integer値 / Integer値」の演算結果は Double 値となりますので、
それを Integer 型の変数 j へと代入するべきでは無いからです。

常に切り捨てとしたいのであれば、/ 演算子ではなく \ 演算子に変更して
 j = i \ n
と記述した方が良いでしょう。

一方、端数も含めて受け取りたいのであれば、
Dim j As Integer を Dim j As Double に変更すれば OK ですね。
この方法だと、「0 ÷ 0」の演算結果を
(Dim n, i, j As Decimal の場合は、0 除算でエラーとなりますが)

元質問にある実装の場合、j は Integer 型でしたので、
i = 5、n = 2 だった場合、i / n は 2.5 へ演算され、j には 2 へと「切り捨て」て格納されます。
i = 7、n = 2 だった場合、i / n は 3.5 へ演算され、j には 4 へと「切り上げ」て格納されます。

これはいわゆる偶数丸めと呼ばれるモードであり、j = i / n の行において
 j = CInt(Math.Round(i / n, MidpointRounding.ToEven))
に相当する処理が行われることになります。

もしも常に四捨五入で演算したいのであれば、
 j = CInt(Math.Round(i / n, MidpointRounding.AwayFromZero))
のように記述する必要があります。ちなみに負数 -4.5 は -5 に丸められます。

「j = i / n」という記述のままだと、意図的に ToEven モードで実装しているのか、
それとも丸め処理を考慮し忘れているのかが第三者には伝わらず、不具合の種と
なりえますので、一応指摘されていただきました。