読者です 読者をやめる 読者になる 読者になる

Quarantine

.NET

C++ との類似性は,あたかもスコープから脱出されるまでは常にオブジェクトが生存しているような錯覚を与えるかもしれません.実際上で見てきたように,自動変数記法で宣言された変数が IDisposable を実装している型の場合に限ればこれは真です.スコープ末尾に Dispose 用のコード片が挿入されることでオブジェクト参照が発生するため,確かにこの場合はスコープから脱出するまではオブジェクトは回収されません.
しかし,ここでもう一度思い出す必要があります.CLR の世界を支配する法則は,有資格観測者から到達できない参照型オブジェクトはいつかは消滅するというものです.そしてスコープなどという観測者は実際には存在しません.コンパイラが何か気を利かせない限り,ソースコードに現れるブラケットはオブジェクトと何ら結合を持っていないのです.
ここでもう一度整理すると,C++/CLI などのいわゆる(高級)マネージ言語のソースコードは,2 段階の変換を受けます.

  1. MSIL に変換された時点で既にスコープの概念は消滅し,オブジェクトは仮想スタックとローカル変数を行ったり来たりしている.
  2. 実際の寿命は実行環境での root 参照に支配され,JIT コンパイル後の実行コードに依存したスタックや CPU レジスタ状態の影響を受けることになる.

実際オブジェクトの回収タイミングを注意深く調べると,ソースコード上のスコープ範囲内での「回収」を観測できることがあります.菊池さんが分かりやすいサンプルを書かれているので,それを参考にさせて頂きましょう.
http://www.divakk.co.jp/blog/aoyagi/archive/2005/03/25/1892.aspx

using namespace System::Collections::Generic;
public ref class Hoge
{
public:
    List<int> list;
    !Hoge()
    {
        // ファイナライザ中でのこのようなアクセスは本来禁止されている
        // ファイナライザ中でのマネージ型へのアクセスには様々な致命的なシナリオが存在するため,
        // よほどの理由がない限り避けるべきである
        list.Clear(); 
    }
};
public ref class Program
{
    static void Main()
    {
        Hoge hoge;
        List<int>^ list;
        list = %(hoge.list);
        list->Add(1);
        list->Add(2);
        GC::Collect();
        System::Threading::Thread::Sleep(0);
        Console::WriteLine(list->Count); // debug -> 2, release -> 0
    }
};

最適化の On/Off で生成される IL が異なっていたことや,デバッグビルド時の JIT 最適化の抑制など様々な要因が考えられるものの,現に Release ビルドではスコープ範囲内でのオブジェクトの回収を観測することが出来ました.
このことからも分かるように,ファイナライザは「てきとー」に書けばいいというものではありません.きちんとルールに従って「適当」に実装する必要があります.C++/CLI に惚れ込むのもいいですが,その辺は理解しておく必要があるでしょう.
もっとも C++/CLI が活用されるようなシナリオでは,gcroot を初めとして考慮すべきことは沢山あり,そもそも GC については気を遣って当たり前なはずです.きっとファイナライザも注意深く実装してくれることでしょう.きっと.まあネタを振った以上一応フォロー*1ということで……

*1:例えば CriticalFinalizerObject 派生型は通常のファイナライザ内からアクセス可能なマネージオブジェクトという側面を持つ,と言えるかもしれませんね.詳しい理由は MSDN 等で熟知すべし書いた当時の意図がよく思い出せないので一旦削除