IDisposableという、さかしいインターフェースをご存知だろうか?
C#からC++を呼び出すなどで外部DLLを扱う場合に割り当てたメモリーや、ファイルハンドル、Bitmapクラスなどアンマネージドリソースを明示的に開放する方法を定義したもので、MSDNには、Dispose()というメソッドのみが用意されている。
要は、このメソッドの中で、クラスが持っているリソースを開放するだけなのだけれど、単にこのメソッドを実装するだけではダメな意地のわるいインターフェース。
今回は、そんな困ったチャンを正しく使う方法をまとめる。方法だけ知りたいせっかちな御仁は最後のセンテンスだけ読めばOK。
まずはDispose()を実装する
兎にも角にも、まずは実装してみる。
メンバーにアンマネージドリソースを持っているクラスを考える。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
internal class SomeClass : IDisposable { private IntPtr unmanagedResource; private StreamWriter managedResource; public void Dispose() { this.Free(this.unmanagedResource); if (this.managedResource != null) { this.managedResource.Dispose(); } } private void Free(IntPtr unmanagedResource) { // 省略 } } |
IDisposableの狙いどおり、Dispose()の中でアンマネージドリソースを開放するだけのもの。開放はFree()メソッドの中で行う。
また、アンマネージドリソースだけでなく、同じくIDisposableを実装しているクラスもここで開放する。
※リソース取得部分は省略している。
デストラクターの実装
IDisposableインターフェイスを実装しただけでは、確実に開放できるわけではない。
インターフェイスによって開放の方法を外部へ公開できただけで、処理の中でDispose()を呼んであげないとメモリリークを引き起こす。
つまり、準備だけしてあとは使う側次第だというわけ。片手落ちというかなんというか、嘆いても仕方がないので、こういうコードによってそこんとこ保証する。
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 |
internal class SomeClass : IDisposable { private IntPtr unmanagedResource; private StreamWriter managedResource; ~SomeClass() { this.Dispose(); } public void Dispose() { this.Free(this.unmanagedResource); if (this.managedResource != null) { this.managedResource.Dispose(); } } private void Free(IntPtr unmanagedResource) { // 省略 } } |
先のコードとの違いは、デストラクターの中でDispose()を呼んでいるところ。
こうすれば、万一Dispose()を忘れた場合でも、ガベージコレクターによってDispose()が呼ばれるので、開放忘れを防げる。
ただ、これにはバグがある。
それは、開発者が呼んだDispose()が実行された後、バックグラウンドで走るガベージコレクターによって、解放済みの(nullになっている)this.managedResourceにアクセスしてしまってnullボカンなケース。
ガベージコレクターがバックグラウンドで実行されるものだから、普通にやってるとこの危険性は残る。
ひえ〜。こんなメンドクサイ落とし穴が待ってたなんて、まいっちゃうね。
Dispose()が呼ばれるケースを切り分ける。
このバグを起こさないために、二つの経路で呼び出される処理がバッティングしないよう、Dispose処理を分ける。
一つは、Dispose()から呼ばれたときの処理。そしてもう一つはデストラクターから。
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 |
internal class SomeClass : IDisposable { private IntPtr unmanagedResource; private StreamWriter managedResource; ~SomeClass() { this.Dispose(false); } public void Dispose() { this.Dispose(true); } private void Dispose(bool isDisposing) { this.Free(this.unmanagedResource); if (isDisposing) { if (this.managedResource != null) { this.managedResource = null; } } } private void Free(IntPtr unmanagedResource) { // 省略 } } |
privateなDispose()メソッドを用意し、引数のフラグ(isDisposing)によって処理を切り分けた。こうして、開発者による破棄処理とガベージコレクターによる処理がバッティングすることはなくなった。
がしかし……。
「踏み込みが足りん!」
10年ぐらい前のスパロボ作品 スーパーロボット大戦F完結編のザコ敵のはずのザコ敵 エリート兵のこのセリフに何度泣かされたことか。
あれからゲームの難易度がどんどん下がってぬるくなり、人肌ぐらいになったところでスパロボをプレイするのはやめたのだけれど、最新作のスパロボBXはamazonでかなり評価が高い。
隠し要素が多いからなのか、騎士ガンダムが出るからなのか、そんなに面白いなら久々にやってみようかな。
大幅に道を逸れてしまったところで本題に戻ると、先のコードは最後の踏み込みが足りない状態。より安全で拡張性のあるコードにするため、破棄処理が一回しか走らないことと、継承したときのことを考えておく。
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 |
internal class SomeClass : IDisposable { private IntPtr unmanagedResource; private StreamWriter managedResource; private bool disposed = false; ~SomeClass() { this.Dispose(false); } public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool isDisposing) { if (!this.disposed) { this.Free(this.unmanagedResource); if (isDisposing) { if (this.managedResource != null) { this.managedResource.Dispose(); } } this.disposed = true; } } private void Free(IntPtr unmanagedResource) { // 省略 } } |
新たに追加した disposed フラグで、privateなDispose()メソッドが何回呼ばれても一回しか処理が走らないようにし、publicなDispose()の後にGC.SuppressFinalize()によってガベージコレクターによる破棄処理が走らないよう指示している。
それから、privateなDispose()メソッドをprotected virtualに変え、このクラスを継承したクラスが破棄処理を書けるようにした。もちろん、その継承先のクラスから継承元(この場合はSomeClass)のDispose()を呼ぶのをお忘れなく。
参照元 : MSDN, stack overflow
C#の定番技術書
C#でも大変有効な定番技術書