2009年11月29日

FxCopに学ぶ番外編(6) : IDisposable を正しく実装します

   このエントリーをはてなブックマークに追加 Clip to Evernote

メールマガジン「C#プログラミングレッスン」では、今、[FxCopに学ぶ編] を連載中です。
そこで取上げようとしたルールで、下書きまでしたのですが、結局メルマガには書かないことにしたルールがいくつかあります。
そのまま捨ててしまうのはもったいないので、ブログに掲載します。


■IDisposableの実装(2)■
前回の続き
まず、なぜ、以下のクラスは、sealedを付加して継承できないようにしないと いけないのでしょうか?
前回のクラスを再度示します。


たとえば、Sampleを継承した、DelivedSampleを考えてみます。
このクラスにも、Dispoaseしないといけないフィールドが存在したとしましょ う。そうなると、必然的に、IDisposableインターフェースを実装しないとい けません。


しかし、このコードは、派生元のDisposeメソッドを隠すことになってしまい ます。
これだと、派生元のTextReaderオブジェクトを破棄することができませ ん。
つまり、Sampleクラスは、継承に対応していないクラスということになり ます。sealedを付けることで、表面上はこの問題を解決することができます。

無理やりコンパイルを通したとしても、以下のようなコードを書くと、


ポリモーフィズムが働きませんので、派生クラスDelivedSampleのDisposeメソ ッドが呼ばれず、DelivedSampleが保持しているリソースを破棄することがで きなくなってしまいます。

では、継承にも対応したクラスをお見せしましょう。


このコードの肝は、
  1. 複数回、Disposeが呼ばれても問題が発生しない。
  2. Finalaizeメソッド(デストラクタ)を実装することで、DIsposeを呼び出さなかった 場合でも、ガベージコレクション時にクリーンアップ処理をしている。
  3. Dispose を呼び出した場合は、GC.SuppressFinalize を呼び出し、 ファイナライズメソッドの呼び出しを抑制している。
  4. (ファイナライズでやることは、すでにDisposeでやっているから不要ということ)
  5. 実際のクリーンアップ処理を受け持つ、virtualで引数ありのDisposeメソ ッドを用意している。これで継承に対応可能。
の4点です。
これは、Disposeパターンと呼ばれているものです。詳しくは、
http://msdn.microsoft.com/ja-jp/library/fs2xkftw.aspx
をご覧ください。

■FxCopのルール
CA1063: IDisposable を正しく実装します



この記事へのコメント
最後の継承にも対応したクラス例に関してですが、
この例ではデストラクタを実装する必要はないのでは?

デストラクタが必要になるのはアンマネージリソースを
保持している場合だけと理解していたのですが・・・

たしかMSDNに「デストラクタは実装するだけで負荷に
なるから必要な場合だけにしてね」ってあったように思うので
この場合は実装しない方が例として適切な気がするのですが・・・

この辺りのMSDNの記事も散らばっていてきちんと理解しているか
不安なので、もし間違ったことを言っていたら指摘してください。
Posted by satoshi at 2009年12月04日 01:37
たしかに、そうですね。
この例では、デストラクタの中で、Dispose(false); としても、結局何もやっていないですしね。

後で訂正しておきます。
Posted by Gushwell at 2009年12月04日 09:00
やはり、
~DisposableType() {
Dispose(false);
}
はあったうが無難だと思われます。
もし、このクラスが継承され、そこで、アンマネージドなリソースを利用していた場合、このデストラクタがあれば、安心です。
派生クラスで、デストラクタを書き忘れていても、これがあれば、アンマネージドリソースを解放することがでできます。
まあ、アンマネージドリソースを使う場面ってほとんどないので、実質的には無くても問題ないと思いますが...
Posted by Gushwell at 2009年12月05日 10:09
何度読み直してもわからないのですが、
~DisposableType() {
Dispose(false);
}

~DisposableType() {
Dispose(true);
}
でなければいけないのではないのでしょうか? Disposeし忘れたままファイナライズに入ったとき、Dispose(false)ではreaderが解放されません。
Posted by Sampo at 2010年06月03日 14:01
.NET Framework は、ガベージコレクションの機能が働いていますので、
Dispose(true)にしてしまうと、マネージリソースを再度破棄しようとする
のでよくないのではと思います。
そのため、ファイナライザで破棄するのは、ガベージコレクターが破棄して
くれないアンマネージリソースに限定します。

というのが僕の理解です。
Posted by Gushwell at 2010年06月03日 23:22
最初のコードを読んで思ったのですが、継承元のクラスのDisposeを、継承したクラスが隠蔽する場合って new キーワードの記述が警告メッセージとして表示されると思うのです。

それをあえて隠蔽するのは、継承クラス側の責任という考え方は間違っているのでしょうか?

また、Disposeメソッドを持つクラスを継承した場合に、基本クラスのアンマネージリソースを解放するロジックを考えるのは、継承クラス側のやるべきことのように感じました。
Posted by 伊藤達也 at 2010年07月02日 17:09
基本は、「newキーワード付けたあなたの責任ですよ」ということで良いと
思うのですが、このDisposeメソッドの場合は、そう単純じゃありません。

例えば、

public new void Dispose() {
base.Dispose();
handle.Dispose();
}

とすれば解決するんじゃないかと考えるかもしれません、
そうなると、

Sample x = new DelivedSample();
x.Dispose();

というポリモーフィズムを使ったコードが、正しく動かなくなってしまいます。
結局、継承先のクラスでどんなコードを書こうが、Disposeを正しく行うコードを書くことができません。

つまり、「基本クラスのリソースを解放するロジックは継承クラス側」では書くことが出来ないのではと思います。

これは、継承元である Sampleクラスが悪いからです。
Sampleクラスが継承されることを考慮していないことになります。

継承を考慮していないクラスなんだから,
sealed 付ければ、表面上は解決しますよ、ということです。

でも、継承も考慮しないといけないクラスなんだとすると、sealedつけるわけにもいかないので、「Disposeパターン」の出番になります。

そして、このDisposeパターンを実装したクラスを継承する場合は、

7. Dispose メソッドから基本クラスの破棄を呼び出します

に話が続く、ということになります。
Posted by Gushwell at 2010年07月02日 21:29
 

この記事へのトラックバックURL

http://trackback.blogsys.jp/livedoor/gushwell/51931181