ネタ元:Dispose、、、(その2)
IDisposable を実装しているクラスを使用する場合、使い終わったら Dispose() を呼ぶべきか否か。
私の意見は「呼べるときは必ず呼べ」または「呼ばなければならないなら呼べ」だ。「如何なる時も必ず呼べ」ではない。
オブジェクトを様々な別オブジェクトとやり取りし、結局いつ使い終わるかが、アプリケーションレベルでは分からない時が少なからずある。そういう時はどう足掻いても呼べない。A を IDisposable を実装した型のインスタンスだとした場合、あるオブジェクトからは A を破棄してもいいように見えるが、別のオブジェクトが A をまだ使おうとしているかもしれない。こういう時は、GC に任せるのだ。GC は A が使い終わったかどうかをアプリケーションより正確に知る事ができる。ある種のハンドルは早々に破棄しなくても大丈夫なのだ。
では、「呼ばなければならない」とはどういう時か。
1.「ファイナライザによるパフォーマンス劣化を防ぎたい」
知っての通り、ファイナライザを実装してしまうと、GC によるオブジェクト解放が一呼吸遅れる。これを嫌うのならば、Dispose を呼び出し、オブジェクトがFリーチャブルキューに並んでしまう事を防ごう。もちろん Dispose() が GC.SuppressFinalize() を呼び出している事が前提である。しかし、これは「呼ばなければならない」という理由には少し弱いだろう。
2.「アンマネージリソースを違う目的にすぐ使う」
ファイルを開いて、その後削除するような時だ。こういう時は、開いたオブジェクトが破棄するなり閉じるなりしないと削除できないので、必ず Dispose() するか Close() してやらねばならない。
3.「ファイナライザを実装していない」
IDisposable を実装しているが、ファイナライザを実装していない場合は、如何なる場合も Dispose() を呼ばなければならない。そうでなければ、いつまでたっても Dispose() が呼ばれない。
これは一体どういうときなのか。IDisposable を実装しているのにファイナライザを実装していないなんて設計が悪いのでは?そうではない。ファイナライザを用意できない事もあるのだ。
良い例が System.IO 名前空間にある。BinaryWriter クラスをよく見て欲しい。ファイナライザを実装していないのだ。リファレンスには、Finalize() メソッドが存在するが、これは Object.Finalize() そのままだ。オーバーライドしていない(ヘルプには何故か「オーバーライド」の一文があるが、実際はオーバーライドしていない)。
CLR は Object.Finalize() がオーバーライドされていないと、ファイナライザを実装していないとみなし、Fリーチャブルキューに並べる事はしない。仮にFリーチャブルキューに並べたとしても、上書きしていない Finalize() は派生クラス特有のアンマネージリソースを解放する術を持たない。
では何故、BinaryWriter はファイナライザを実装していないのか。
これが「依存関係による問題」である。(参考:「プログラミング .NET Framework」)
Jitta 氏の仰っているように、~Reader、~Writer と Stream は Dispose() の呼び出し順によって結果が変わる。特に顕著なのが、BinaryWriter と Stream の組み合わせの時のバッファの行方だ。
MemoryStream stream = new MemoryStream();
BinaryWriter writer = new BinaryWriter(stream);
書き込み処理
writer.Close();
stream.Close();
上記のようにした場合、writer はバッファに貯めてあるデータをストリームに書き出し、元になるストリームを閉じてしまう。
MemoryStream stream = new MemoryStream();
BinaryWriter writer = new BinaryWriter(stream);
書き込み処理
stream.Close();
writer.Close();
対して、上記のようにした場合は、先にストリームが閉じてしまっているので、バッファの内容は書き込む事が不可能になってしまい、破棄される事になる。
ここまでで勘の良い賢明諸氏は気付いただろう。
その通り。ファイナライザには呼び出し順に何の約束もないのである。
もし、BinaryWriter にファイナライザが実装されていて、Dispose() を呼ぶような仕様になっていたらどうなっていただろう。我々が明示的に Dispose() を呼ばなかったりしたら、GC の気分次第で、ストリームに内容が書き込まれたり、書き込まれなかったりする。これは非常に分かりづらいバグに繋がる。よって Microsoft は敢えて BinaryWriter にファイナライザを実装せず、我々が明示的に Dispose() を呼ばなかった場合は、バッファ内のデータが「必ず」破棄されるようにしたのだ。「必ず」破棄されれば Dispose() を呼ばなければならない事に気付くだろうという事だ。
IDisposable が実装されていたら Dispose() を呼んだ方が良いのは明らかだが、更にファイナライザが用意されていない場合は「必ず」呼ばなければならないのだ。