Visual Basic 中学校 掲示板 投稿内容
タグのない投稿を抽出 統計 RSS

Visual Basic 中学校 > 投稿一覧 >

StreamWriter クラスの Close 呼び出し方 解決済み

エラー ファイル書き込み 例外 タグの編集...

投稿者 ポムNNN   (社会人)   投稿日時 2018/2/4 23:16:46
開発環境 Visual Studio 2015, Windows7(64bit)
StreamWriter クラスの Close メソッドの部分で下記プログラムの Write1(), Write2() の2つのうちどちらがいいいのか迷っています。

Write1() はStreamWriterクラスを使用する直前でインスタンス化するプログラムです。
Write2() はStreamWriterクラスを最初にインスタンス化するプログラムです。

個人的には Write1() がいいかと思っています。
理由としては、第46回クラス作成の中で
 
”クラスの生成というのは効率が悪い作業の1つなのです。プログラム中ではできるだけクラスを生成する回数を減らすようにするように努力してください。”

と記載があるからです。今回の場合はインスタンス化の前段階で例外が発生し、インスタンス化が無駄だと思ったからです。
どなたかアドバイス頂けませんでしょうか?よろしくお願いします。

  Private Sub Write1()

        Dim i As Integer = 0
        Dim n As Integer = 0
        Dim j As Integer
        Dim sw As StreamWriter = Nothing
        Dim FilePath = "" 'ファイルのフルパスを入れる

        Try
            j = i / n
            sw = New StreamWriter(FilePath, True, System.Text.Encoding.GetEncoding("shift-jis"))
            sw.WriteLine(j)

        Catch ex As Exception
            MsgBox(ex.Message)
        Finally

            If Not sw Is Nothing Then
                sw.Close()
            End If

        End Try
    End Sub

///////////////////////////////////////////////////////////////
    
Private Sub Write2()

        Dim i As Integer = 0
        Dim n As Integer = 0
        Dim j As Integer
        Dim FilePath = "" 'ファイルのフルパスを入れる
        Dim sw As StreamWriter = New StreamWriter(FilePath, True, System.Text.Encoding.GetEncoding("shift-jis"))

        Try
            j = i / n
            sw.WriteLine(j)

        Catch ex As Exception
            MsgBox(ex.Message)
        Finally
            sw.Close()
        End Try
    End Sub

投稿者 (削除されました)   ()   投稿日時 2018/2/5 11:33:36
(削除されました)

投稿者 魔界の仮面弁士   (社会人)   投稿日時 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 モードで実装しているのか、
それとも丸め処理を考慮し忘れているのかが第三者には伝わらず、不具合の種と
なりえますので、一応指摘されていただきました。

投稿者 ポムNNN   (社会人)   投稿日時 2018/2/5 22:55:14
魔界の仮面弁士様

>これのことですね。http://rucio.a.la9.jp/main/dotnet/shokyu/standard46.htm
URLを載せず申し訳ありません。まさにこのURL内のことです。

今回の質問に際し説明不足でしたので補足させていただきます。
StreamWriterクラスのインスタンス化前やインスタンス化後に想定外の例外の発生をさせてしまった場合のStreamWriterクラスのCloseメソッドの正しい実装方法を質問したかったのですが、説明不足により混乱を招き申し訳ありません。なので j = i / n の計算の部分につきましてはプログラム作成段階で思いついた適当な例外になります・・・実際はご指摘の通り”If”等を使いプログラムをしていきます。

>.NET の例外処理の考え方について、下記が参考になると思います。
いくつかの参考URLをあげていただき、ありがとうございます。しっかり読み、理解したいと思います。

>その上で、実際にどう実装するのかは案件次第ですが、たとえば、エラーの理由が・・・(省略)
(a)~(d)のその後の処理方法まで教えていただき、ありがとうございます。 参考にしたいと思います。

>以下蛇足:
この部分の記載につきましては知らなかった事、認識が甘いものがいくつかありました。勉強になりました。

投稿者 魔界の仮面弁士   (社会人)   投稿日時 2018/2/6 07:42:23
記述漏れがあったので補足します。

4000文字上限回避のために文章を削っている際に、編集し損ねていたようで…。


> 一方、端数も含めて受け取りたいのであれば、
> Dim j As Integer を Dim j As Double に変更すれば OK ですね。
> この方法だと、「0 ÷ 0」の演算結果を

この方法だと、「0 ÷ 0」の演算結果を受け取ってもエラーになりません。
と書くつもりでした。

'下記はエラーになる 
Dim i As Integer = 0
Dim n As Integer = 0
Dim j As Integer
j = i / n  'OverflowException:算術演算の結果オーバーフローが発生しました。 




'下記はエラーにならない 
Dim i As Integer = 0
Dim n As Integer = 0
Dim j As Double  'As Integer から変更 
j = i / n  'j = Double.NaN 




> (Dim n, i, j As Decimal の場合は、0 除算でエラーとなりますが)
j だけでなく、n や i も Decimal にしていることに注意してください。
何故ならば、
> 「Integer値 / Integer値」の演算結果は Double 値となりますので
であるからです。しかし「Decimal 値 / Decimal 値」の演算結果は Decimal 型です。

'下記はエラーになる 
Dim i As Decimal = 0D
Dim n As Decimal = 0@
Dim j As Decimal  'As Integer から変更 
j = i / n  'DivideByZeroException:0 で除算しようとしました。 



エラーの種類が変化していることに注意してください。

変数がすべて Integer だった場合は、「演算結果(Double)をIntegerに代入したとき」に
OverflowException が発生していたのに対して、
変数をすべて Decimal にしていた時は、代入時ではなく「演算したとき」に
DivideByZeroException が発生することになります。


それと、ここでは意図的に 0 ではなく 0D や 0@ という表現にしています。
0 は Integer 型のリテラル、0.1 だと Double 型のリテラル、
0.1D や 0.1@ なら Decimal 型のリテラルを意味します。

投稿者 ポムNNN   (社会人)   投稿日時 2018/2/7 22:49:23
魔界の仮面弁士様
今回の質問であるStreamWriter クラス件以外にもたくさんの補足情報等ありがとうございました。
計算だけでも知らないことが多数ありました。勉強不足を痛感しました。
またわからないことがありましたら質問させていただきます。ありがとうございました。