要らなくなったら勝手に消えるDictionary

Dictionary で外付けプロパティを

 ある程度C#を使っていれば必ず使うことになる、System.Collections.Generic.DictionaryC++で言うところの std::map コンテナに対応するコレクションです。

 キーと値の対応付けが簡単に作れるので、既存のオブジェクトへの外付けプロパティ的な使い方をしたくなるのは当然と言えるでしょう。

 こんな感じですね:

var prop = new Dictionary<ICollection<object>, int>();

var hoge = new HashSet<int>();
var piyo = new List<int>();

prop[hoge] = 10;
prop[piyo] = 20;

Console.WriteLine(prop[hoge] + prop[piyo]); // 30

 こんなの、クラスを継承すればいいじゃないか、と思うかもしれませんが、ライブラリの内部で生成されたインスタンスに追加の値を設定したい場合など、継承でどうにかならない場合も多くあります。例えば、jQuery.data() よろしく WebBrowser から取得した HtmlElement に追加データを設定したい時とか。

 ただ、Dictionary を生で扱うのはさすがによろしくないので、クラスにしてしまいましょう。

class ExternalProperty<TKey, TValue> where TKey : class
{
  private readonly Dictionary<TKey, TValue> _dictionary = new Dictionary<TKey, TValue>();

  public TValue this[TKey key]
  {
    get
    {
      TValue prop;
      if ( _dictionary.TryGetValue( key, out prop ) ) return prop;
      return default( TValue );
    }
    set
    {
      _dictionary[obj] = value;
    }
  }
}

 これで問題ありません……果たしてそうでしょうか?

 問題は、Dictionary のキーにオブジェクトをそのまま使っていることにあります。

 というのも、これでは Dictionary がキーの参照を保持し続けているので、キーとなるオブジェクトが不要になった時には上記のExternalProperty から確実にキーを削除しなければメモリリークが発生します。

 最も単純明快な解決策は、キーオブジェクトが不要になったタイミングで手動で ExternalProperty からも消去することです。が、全ての参照が消えたことを、本当に絶対確実に過不足なく手動管理できるのでしょうか?

GCを妨害しない外付けプロパティ

 そんな時にこそ、WeakReference の出番です。WeakReference が保持する参照はガベージ・コレクションの対象になるため、WeakReference.IsAlive の値を確認することでキーオブジェクトが不要になったことを判断できるのです。

 そこで、

  private readonly Dictionary<WeakReference, TValue> _dictionary = new Dictionary<WeakReference, TValue>();

 ……ではダメ。なぜなら WeakReference は保持するオブジェクト同士ではなく、WeakReference そのもののインスタンスを比較するため、次のようなコードでは false になってしまうのです!

var obj = new object();
Console.WriteLine(new WeakReference(obj) == new WeakReference(obj));

 解決策は、Microsoft 自身によって提示されています。

http://msdn.microsoft.com/ja-jp/magazine/cc163324.aspx

もう 1 つの解決策はここで実装するもので、問題をその根本から解決します。WeakReference での GetHashCode と Equals の実装です。これを行うため、図 7 に示す ObjectEqualityWeakReference というクラスを作成しました。このクラスは、WeakReference の Equals メソッドと GetHashCode メソッドをオーバーライドして必要なセマンティクスを提供します。ObjectEqualityWeakReference は、構築されると、指定されたオブジェクトのハッシュ コードをメンバ変数にキャッシュします。このキャッシュした値がオーバーライドした GetHashCode から返される値になります。これにより、ハッシュ コードが基になるオブジェクトに基づきます。オブジェクトが収集されても、ObjectEqualityWeakReference は同じハッシュ コード値を返し続けます。オブジェクトのハッシュ コードは変わるべきではなく、Dictionary では、そのテーブル内でオブジェクトを検索する最初の手段としてハッシュ コードを使用します。オブジェクトの追加後にそのオブジェクトのハッシュ コードが変わると、Dictionary でそのオブジェクトを見つけられない可能性が高くなります。

 上記記事とは WeakReference を使う目的は違いますが、同じように実装してやればよいでしょう。

 その上で、定期的に _dictionary を掃除し、不要になったキーに対応する値を消去してやる処理を加えればメモリリークはなくなります。掃除は、ベストはGCが走る時に同時に行うことなのですが、C#にそういった仕組みはないので、次善の策としてプロパティへのアクセス時にGCが走った後かどうかを確認することにしましょう。

 もちろん、毎回全ての WeakReference を舐めて消えたものがあるか確かめるのは無駄なので、前回チェックした時以降にGCが走ったかどうか、という点だけに着目します。これは、GC.CollectionCount() メソッドを使うことでできます。

class ExternalProperty<TKey, TValue> where TKey : class
{
  private readonly Dictionary<TKey, TValue> _dictionary = new Dictionary<TKey, TValue>();

  private int _lastCollectionCount;

  public TValue this[TKey key]
  {
    get
    {
      TValue prop;
      if ( _dictionary.TryGetValue( key, out prop ) ) return prop;
      return default( TValue );
    }
    set
    {
      CheckGC();
      _dictionary[obj] = value;
    }
  }

  private static int GetCollectionCount()
  {
    return GC.CollectionCount( 0 );
  }

  private void CheckGC()
  {
    int cc = GetCollectionCount();
    if ( _lastCollectionCount == cc ) return;

    var list = new List<ObjectEqualityWeakReference>();
    foreach ( var key in _dictionary.Keys ) {
      if ( !key.IsAlive) list.Add( key );
    }

    foreach ( var key in list ) {
      _dictionary.Remove( key );
    }

    _collectionCount = GetCollectionCount();
  }
}

 ここまでできてしまえばメモリリークはないので、ExternalProperty をシングルトンか何かにしてやっても大丈夫でしょう。

 最後に、プロパティ名[オブジェクト] という形で外付けプロパティにアクセスするのは気持ち悪いので、拡張メソッドを使ってどうにかしておきます。

static class ExternalPropertyExtension
{
  private static ExternalProperty<ICollection<object>, int> _ext = new ExternalProperty<ICollection<object>, int>();

  public static int GetExtetnalProperty(this ICollection<object> col)
  {
    return _ext[col];
  }

  public static void SetExtetnalProperty(this ICollection<object> col, int value)
  {
    _ext[col] = value;
  }
}