VC.NET ~ ディスポーズ、ファイナライズ、デストラクト

前置き

ディスポーズ(dispose)、ファイナライズ(finalize)、デストラクト(destruct)。どれも似たような概念だし、プログラミング言語によって定義が微妙に異なるので混乱しがちです。私自身、イマイチ納得できていません。ひとまず、現状の理解を整理してみます。

本文

すべてはリソースの解放のため

ディスポーズも、ファイナライズも、デストラクトも、根源の目的は、使わなくなったリソースを漏れなく解放することです。ここで言うリソースとは、ヒープ上のメモリだけでなく、ファイルのハンドルやスレッド、セマフォ、データベースとのコネクションなどを含みます。すなわち、alloc/newしたらfree/deleteする、openしたらcloseする、createしたらdestroyする、といったようなライフサイクルを持つ物すべてを指します

まずは、ディスポーズ、ファイナライズ、デストラクト、それぞれを、ゆるやかに定義してみましょう。

ディスポーズ
プログラマーの視点から見ると、クラスが内部でいくつかのリソースを使う場合、それらをまとめて解放するメソッドを定義し、一発で明示的に解放できるように設計するでしょう。これがディスポーズ処理です。
デストラクト
クラスのインスタンスは、ヒープやスタック上に生成されるわけですが、このインスタンス自体も、使わなくなったりスコープから抜ける時点で明示的に(あるいは暗黙のうちに)廃棄されます。このときに呼ばれるのがデストラクト処理です。
ファイナライズ
GC(ガベッジコレクション)をサポートするシステムでは、ヒープ上のオブジェクトメモリを回収するときに、そのオブジェクトにリソース解放のチャンスを与えるのが通例です。これがファイナライズ処理です。また、ファイナライズ呼び出しには確実性がないとされることが多いようです(確実に呼ばれるとは限らない)。

これらは、あくまでも、ゆるやかな定義です。特にデストラクタは、ディスポーズやファイナライズとの境界がハッキリしませんね。

CLI的には

.NETの実行環境であるCLI(あるいはCLR)のレイヤから見ると、ディスポーズ処理を行うのがIDisposable::Dispose()で、ファイナライズ処理を行うのがObject::Finalize()です。CLIにはデストラクト処理に相当するメソッドはありません。

GCの対象となるのは、マネージ・ヒープに配置された参照クラス(CLIクラス)のインスタンスのみで、これをマネージ・リソースと呼びます。その他のリソース(ネイティブヒープ、ファイルハンドル、ネットワークコネクションなど)は全てアンマネージ・リソースです。

ディスポーズ時は、マネージ・リソースとアンマネージ・リソースを両方とも解放します。マネージ・リソースはGCが自動解放してくれるとは言え、そのマネージ・リソースが内部でアンマネージ・リソースを使ってる可能性もあるので、使わないなら早めに解放すべきです。一方、ファイナライズ時は、アンマネージ・リソースのみを解放します。ファイナライズが行われるのはGCの中なので、その時点ではマネージ・リソースが中途半端な状態になっている可能性があるからです。

またディスポーズは、何度呼ばれても(あるいは、中途半端にコンストラクトされた状態のオブジェクトに対して呼ばれても)破綻しないように実装すべきです。なぜならCLIは、コンストラクタの途中で例外が発生した場合に、そのオブジェクトをディスポーズするからです。

ディスポーズとファイナライズ。マネージ・リソースとアンマネージ・リソース。この組み合わせだけなら割と単純なのですが、ここにクラスの継承ツリーが絡んでくると、どこで何を解放すべきなのか混乱してしまいます。そこで、マイクロソフトは、CLI Dispose Patternというデザインパターンを推奨しています。基本クラスBaseと派生クラスDerivedを例に、仮想的なコードを使って説明します。なお、このコードでは、例外発生時の処理やスレッドセーフにするための処理は省いています。

  • CLI Dispose Pattern
class Base : public IDisposable          // IDisposableを継承。
{
public:
  Base() {}

  void Dispose() {      // IDisposable::Dispose()を実装。
                        // 非virtual(派生クラスでのオーバーライドは禁止)。
    Dispose(true);                       // ディスポーズ処理の本体。
    GC::SuppressFinalize(this);          // GCに、ファイナライズ抑止を指示。
  }

protected:
  void Finalize() {                      // Object::Finalize()をオーバーライド。
    Dispose(false);     // ここでもディスポーズ処理。
  }

  virtual void Dispose(bool disposing) { // 新たなメソッド。オーバーライド可。
                        // ディスポーズ処理を行う。
                        // disposingには、Dispose()から呼ぶときはtrueを、
                        // Finalize()から呼ぶときはfalseを指定。
    if ( disposing ) {
      [マネージ・リソースを解放]
    }
    [アンマネージ・リソースを解放]
  }
};

class Derived : public Base              // Baseを継承。
{
public:
  Derived() {}

protected:

  virtual void Dispose(bool disposing) { // 基本クラスのDispose(bool)をオーバーライド。
    if ( disposing ) {
      [マネージ・リソースを解放]
    }
    [アンマネージ・リソースを解放]

    base.Dispose(disposing);             // 基本クラスのディスポーズ処理へ。
  }
};

ポイントは以下の点です。

  • 継承ツリーの、ディスポーズ処理を必要とするクラスの中で、最も上位のクラスのみがIDisposable::Dispose()を実装し、Object::Finalize()をオーバーライドする
  • それ以外のクラスは、Dispose(bool disposing)をオーバーライドする
  • Dispose()の中では、当該オブジェクトをGCするときにファイナライザを呼ばないよう、GCに指示する
  • Dispose(bool disposing)の中では、マネージ・リソースとアンマネージ・リソースを解放する
  • 派生クラスのDispose(bool disposing)の最後で、基本クラスのDispose(bool disposing)を呼ぶ

このパターンを使うと、クラスDerivedのインスタンスに対してDispose()を呼べば、Derived::Dispose(true)とBase::Dispose(true)が呼ばれます。また、クラスDerivedのインスタンスがGCで回収されるときは、Derived::Dispose(false)とBase::Dispose(false)が呼ばれます。

個人的には、このパターンは気持ち悪いです。派生クラスではIDisposable::Dispose()をオーバーライドできないとか、Disposable(bool disposing)がIDisposableで宣言されてないとか。これはもう、単なるデザインパターンではなく、CLI仕様の一部と言ってもいいくらい強制力があります。もしクラスBaseとクラスDerivedの開発者が異なる場合、お互いにパターンの理解を共有しておかないと大変なことになりそうです。

さらにやっかいなことに、このパターンに適合するようなコードを書く方法は、言語ごとに異なるのです。それもあって、上記では仮想的なコードでパターンを示しました。この後は、各言語による実現方法について説明します。

C++/CLIでは

C++/CLIでは、IDisposable::Dispose()も、Object::Finalize()も、直接オーバーライドすることはできません。その代わり、デストラクタ構文をディスポーズ用に、!を使った新しい構文をファイナライズ用に使います。

ref class A {  // 参照クラス(CLIクラス)。
public:
  A() {}
  ~A() {}      // ディスポーザ。
               // 構文はデストラクタだが、意味的(役割的)にはディスポーザ。

protected:
  !A() {}      // ファイナライザ。
};

「デストラクタ構文をディスポーズ用に使う」と書きましたが、~A()が、CLIのIDisposable::Dispose()に変換されるわけではありません。C++/CLIコンパイラは、クラスの継承ツリーのどこに ~ や ! があるかを調べて、前述のCLI Disposable Patternに適合するようにコンパイルします。

例えば、A←B←Cという継承ツリーがあり、各クラスが~A()、~B()、~C()を実装している場合なら、C++/CLIコンパイラは、以下のようなコードを生成します。

  • クラスAがIDisposable::Dispose()を実装
  • クラスA、B、CそれぞれがDispose(bool disposing)を持つ
  • A::Dispose()の中身は前述のBase::Dispose()と同じ
  • 各クラスのDispose(bool disposing)の中では、disposingがtrueなら各クラスのディスポーザを呼び、基本クラスのDispose(true)を呼ぶ

一方、各クラスが!A()、!B()、!C()を実装している場合の生成コードは以下の通りです。

  • 各クラスのDispose(bool disposing)の中では、disposingがfalseなら各クラスのファイナライザを呼び、基本クラスのDispose(false)を呼ぶ

例えば、クラスCのDispose(bool disposing)は以下のようなコードとしてコンパイルされます。

virtual void Dispose(bool disposing) {
  if ( disposing ) {
    [~C()の中身]

    B::Dispose(true);
  }
  else {
    [!C()の中身]

    B::Dispose(false);
  }
}

よって、クラスの実装者に求められるのは、

  • ディスポーザで、マネージ・リソースとアンマネージ・リソースを解放することと
  • ファイナライザで、アンマネージ・リソースを解放すること

です。基本クラスのディスポーザやファイナライザにチェーンする処理はコンパイラが生成してくれるので、自クラスのリソース解放のみをケアすればOKです。

アンマネージ・リソースの解放処理はファイナライザに実装するので、ディスポーザにはマネージ・リソースの解放処理を書いて、ファイナライザを呼ぶようにすれば良いでしょう。

~A() {
  [マネージ・リソースを解放]

  this->!A();           // アンマネージ・リソースの解放はファイナライザに任す。
}

!A() {
  [アンマネージ・リソースを解放]
}

C++/CLIのディスポーザを実行するには、gcnewしたインスタンスをdeleteしたり、ローカル変数がスコープから抜けたり、ディスポーザを明示的に呼ぶといった方法があります。

void foo()
{
  A a1;                 // a1は、foo()がreturnするときに自動的にディスポーズされる。
  A^ a2 = gcnew A();
  A^ a3 = gcnew A();
  delete a2;            // a2をディスポーズする。
  a3->~A();             // a3をディスポーズする。
}

余談ですが、標準C++プログラマの目には、a1はスタック上に配置されるように見えます。しかし実際は、マネージ・ヒープ上に置かれます。それは、Aクラスをref class(つまりreference type)として定義したためです。value class(つまりvalue type)として定義した場合は、a1がスタック上に配置されます。

C#では

C#では、(C++/CLIとは違って)CLIのIDisposableを直接使うことができます。しかし、Object::Finalize()を直接オーバーライドすることはできないので、代わりにデストラクタ構文を使ってファイナライザを実装します。

public class A : IDisposable {
  public void Dispose() {}  // ディスポーザ。

  ~A() {}                   // ファイナライザ。
                            // 構文はデストラクタだが、コンパイラが
                            // Object::Finalize()に変換する。
}

C#コンパイラは、デストラクタを、オーバーライドしたFinalize()としてコード生成します。そのコードの最後では親クラスのFinalize()を呼ぶようになっています。つまり、~A() { ... }は、以下のようにコンパイルされます。

protected override void Finalize()
{
  try {
    [~A()の中身]
  }
  finally {
    base.Finalize();
  }
}

では、C#で、前述のCLI Disposable Patternに適合するコードを書いてみましょう。

  • C#によるCLI Dispose Patternの実装
public class Base : IDisposable
{
  public void Dispose() {
    Dispose(true);
    GC.SuppressFinalize(this);
  }

  protected virtual void Dispose(bool disposing) {
    if ( disposing ) {
      [マネージ・リソースを解放]
    }
    [アンマネージ・リソースを解放]
  }

  ~Base() {
    Dispose(false);
  }
}

public class Derived : Base
{
  protected override void Dispose(bool disposing) {
    if ( disposing ) {
      [マネージ・リソースを解放]
    }
    [アンマネージ・リソースを解放]

    base.Dispose(disposing);
  }
}

派生クラス側にデストラクタ(ファイナライザ)は不要です。なぜなら、基本クラスのデストラクタ経由でDerived::Dispose(false)が呼ばれ、そこでアンマネージ・リソースを解放できるからです。また、派生クラスがデストラクタを実装すると、その中で自動的に基本クラスのデストラクタを呼ぶようなコードが生成されるので、Dispose(false)が2重に呼ばれてしまいます。ディスポーズ処理は何度呼ばれても大丈夫なように実装するのが基本ですが、明らかに冗長なディスポーズの繰り返しは避けた方が良いでしょう。

C++/CLIでは、参照クラスのインスタンスをスタック上に配置することにより、スコープから抜けるときに自動的にディスポーズされるようなコードを書くことができましたが、C#ではクラスのインスタンスをスタック上に配置することができません。そこで、代替手段としてusing構文が用意されています。

using (Derived d = new Derived()) {
  ...
}         // このブロックから抜けるときにd::Dispose()が呼ばれる。

うげっ、と言いたくなる構文ですね。ディスポーズが必要なオブジェクトが複数個ある場合は…

using (Derived d1 = new Derived())
using (Derived d2 = new Derived())
using (Derived d3 = new Derived()) {
  ...
}         // d3、d2、d1の順にDispose()される。

だそうです。

VBでは

VBでは、IDisposable::Dispose()もObject::Finalize()も、直接オーバーライドすることができます。なので、CLI Disposable Patternの実装も、直接的に行うことができます。

  • VBによるCLI Dispose Patternの実装
Public Class Base
  Implements IDisposable

  Public Overloads Sub Dispose() Implements IDisposable.Dispose
    Dispose(True)
    GC.SuppressFinalize(Me)
  End Sub

  Protected Overloads Overridable Sub Dispose(disposing As Boolean)
    If disposing Then
      [マネージ・リソースを解放]
    End If
    [アンマネージ・リソースを解放]
  End Sub

  Protected Overrides Sub Finalize()
    Dispose(False)
  End Sub
End Class

Public Class Derived
  Inherits Base

  Protected Overloads Overrides Sub Dispose(disposing As Boolean)
    If disposing Then
      [マネージ・リソースを解放]
    End If
    [アンマネージ・リソースを解放]

    Mybase.Dispose(disposing)
  End Sub
End Class

VBにも、C#と似たようなusing構文があります。

Using d As New Derived()
  ...
End Using

複数の場合は。

Using d1 As New Derived(), d2 As New Derived(), d3 As New Derived()
  ...
End Using