週記(1/5〜1/9) その1
いまさらながら、あけましておめでとうございます。
月曜始発で出発して金曜終電で帰宅する。
年末からそういう生活ですので、日記というより週記状態になりつつあります。
自身の勉強も兼ねて、.NET のベースライブラリから1日記1つくらいを取り上げていきたいな、と思います。
GC はアンマネージドリソースの不足に対応できない
アンマネージドリソース F は FCreate で作成され、FDestroy で解放でき、100個の有限なりソースであるとします。
class Foo { public Foo() { CreateF(); } ~Foo() { ReleaseF(); } ///アンマネージドオブジェクト F private int f_handle = F_EMPTY; protected void CreateF() { if (f_handle == F_EMPTY) foo_handle = FCreate(); } protected void ReleaseF() { if (f_handle != F_EMPTY) { FDestroy( f_handle ); f_handle = F_EMPTY; } } public some_method... }
クラス FOO はアンマネージドオブジェクト F をカプセル化するクラスで、自身と F の寿命を同じくしたいと考えていますが、F の解放をファイナライザで行っているため、F は Foo より長い寿命を持ちます。
for (int i=0; i<500; i++) { Foo foo = new Foo(); foo.someMethod(); // foo = null; }
これは、Foo を利用してオブジェクト F を作成し、その機能を1つ呼び出すということを500回繰り返すテストコードです。
個々の Foo の寿命はコメントになっている null の代入文までです。foo の参照が失われオブジェクトは GC によって いつか 回収されます。この いつか というのが大きな問題で、オブジェクト F は Foo と共に生まれたにも関わらず、その寿命は GC によって管理される いつか まで続きます。
結果として、このループは2個以上の F を作成してしまうことになり、多くの環境では101個目の F を作成しようとしてしまった結果、正常に動作を行うことはできないでしょう。
この問題を解決するために、前述のようにクラスはマネージドリソースとは別に、アンマネージドリソースを管理する手段を提供する必要があります。
そのための手法は多くありますが、1つの手段としてクラス利用者によって早い段階で破棄を可能とする……つまり、手動による Dispose() の呼び出しがあります。
別に Dispose() ではなく Close() や Free() や Release() でも問題はないのですし、IDisposable.Dispose() でもそのように説明されています。
ファイルやソケット接続のように、閉じる (Close) という行為がアンマネージドリソースに対する破棄行為と直結している場合には、IDisposable.Dispose() が Close() を呼び出すような実装(またはその逆)でもクラス利用者は混乱しないかもしれません。しかし、常に Close() と Dispose() が同じ意味を持つとは限りませんので、クラス利用者が Close() メソッドと Dispose() メソッドのどちらを呼べばいいのか? どちらも呼ばなければならないのか? を考えなければならないようになる可能性があります。それを防ぐのが IDisposable を実装するということです。
IDisposable.Dispose() は冒頭に書いたように、そのオブジェクトをファイナライズします。Close() によってアンマネージドリソースの状態が変化するだけであろうが、アンマネージドリソースの解放が行われていようが、IDisposable.Dispose() は常にマネージドリソースを破棄していることを期待するための機能として呼び出せるようにクラスが設計されています。そして、そう設計・実装するべきであろうと思います。
このような、クラス利用者が Close() 等の固有のメソッドの機能性を詳しく知ることなく、オブジェクトの破棄を指示するための統一した名前が存在することは学習面でも利用面でも大きな利点であり、IDisposable というインターフェスで定義することによって、十分なドキュメンテーションを必要とせず手動による破棄処理の必要性をクラス設計が主張することを可能とします。
このクラス設計における主張には、Dispose() という関数がファイナライザの手動呼び出しの意味に使われていること、逆に言えばそれ以外の目的に呼び出してはならないことも主張していると言うことが言えます。
所有権や破棄可能権のないオブジェクトを Dispose できない
これは、IDisposable はオブジェクトの寿命を管理するための機能・主張であって、使用を管理するための機能ではないことを知っておかなければ危険であることも示します。
someMethod( IDisposable obj) { // : obj.Dispose(); }
someMethod() が Dispose() を呼び出していますが、このような呼び出しが許されるのは非常に稀で、通常はメソッドはオブジェクトの破棄を行いません。多くの場合、このような実装はコーディングミスであり、バグです。もっと危険な例は C# の using() 構文です。
someMethod( IDisposable obj) { using(obj) { // : } }
using() は IDisposable とセットで使うことができる便利な構文ですが、その using という名前とは異なり Dispose() の呼び出しを含んでいます。
ここで提案できる using() に関する唯一のガイドラインは、オブジェクトの完全な寿命を using() で定義するということです。
using(object obj = new Foo()) { // : }
using() はこのように、オブジェクトの生成と共に扱われるように記述しよう、ということです。このような記述ができない場合は using() を使うべきではないと考えています。
- id:ladybug:20040111 へ続きます。