2つのキーを利用できるConditionalWeakTableクラス

.NETのConditionalWeakTableは、キーを弱参照で保持し、キーがGCされたときに自動でキーと値がRemoveされるようになっています。

しかし、キーとして複数の参照を持ち、参照のうちどれかがGCされたらRemoveを行うといった使い方はできません。

今回は複数の参照キーを指定できるConditionalWeakTableのようなものを実装してみたいと思います。

ConditionalWeakTableの中身

内部的にDependentHandleという構造体で管理しているようですが、privateのため利用できません。

Ephemeronというデータ構造を利用しているらしいですが、詳しくは追及していません。

2つのキーを弱参照で保持するクラス

Keyを弱参照で保持し、EqualsやGetHashCodeをもともとのKeyから算出するクラスを作成します。

class Keys
{
    public WeakReference<TKey1> Key1 { get; }
    public WeakReference<TKey2> Key2 { get; }
    int _hash;

    public bool IsAlive()
    {
        return Key1.TryGetTarget(out var key1)
            && Key2.TryGetTarget(out var key2);
    }

    public Keys(TKey1 key1, TKey2 key2)
    {
        Key1 = new WeakReference<TKey1>(key1);
        Key2 = new WeakReference<TKey2>(key2);

        // WeakReferenceではなく、もとのkeyからhash作成
        _hash = 365011897;
        _hash = _hash * -1521134295 + key1.GetHashCode();
        _hash = _hash * -1521134295 + key2.GetHashCode();
    }

    public override bool Equals(object obj)
    {
        // WeakReferenceの比較ではなく、中身の参照の比較
        var keys = obj as Keys;
        return keys != null
            && TargetEquals(Key1, keys.Key1)
            && TargetEquals(Key2, keys.Key2);
    }

    bool TargetEquals<T>(WeakReference<T> weakRef1, WeakReference<T> weakRef2)
        where T : class
    {
        return weakRef1.TryGetTarget(out var ref1)
            && weakRef2.TryGetTarget(out var ref2)
            && Equals(ref1, ref2);
    }

    public override int GetHashCode()
    {
        return _hash;
    }
}


WeakTableクラス

上記のKeysクラスを利用して、WeakTable本体を実装していきます。

class WeakTable<TKey1, TKey2, TValue>
    where TKey1 : class
    where TKey2 : class
{
    Dictionary<Keys, TValue> _dictionary = new Dictionary<Keys, TValue>();

    public void Add(TKey1 key1, TKey2 key2, TValue value)
    {
        _dictionary[new Keys(key1, key2)] = value;
    }

    public bool TryGetValue(TKey1 key1, TKey2 key2, ref TValue value)
    {
        var key = new Keys(key1, key2);
        if (_dictionary.ContainsKey(key) == false)
            return false;
        value = _dictionary[key];
        return true;
    }

    public void CheckReferences()
    {
        // 不必要なレコードを削除
        _dictionary = _dictionary
            .Where(x => x.Key.IsAlive())
            .ToDictionary(x => x.Key, x => x.Value);
    }
}


GCが行われたときにCheckReferencesを呼ぶ

現状のままでは、利用者がCheckReferencesを呼ばないと不必要なレコードが解放されないので、GC時に呼ばれるよう実装してみます。

GCの検知の詳細は下記記事を参照してみてください。
Garbage Collectionを検知する(C#) - 何でもプログラミング

今回はこのようなクラスを用意しました。

class GCNotifier
{
    public static event EventHandler Collected;

    static GCNotifier()
    {
        new DummyObject();
    }

    class DummyObject
    {
        ~DummyObject()
        {
            if (!AppDomain.CurrentDomain.IsFinalizingForUnload()
            && !Environment.HasShutdownStarted)
            {
                Collected?.Invoke(null, EventArgs.Empty);
                new DummyObject();
            }
        }
    }
}


GCNotifierを利用して、WeakTableに実装を追加します。

WeakEventManagerでCollectedにアタッチし、lock機構を追加しています。

class WeakTable<TKey1, TKey2, TValue>
    where TKey1 : class
    where TKey2 : class
{
    object _lockObj = new object();
    Dictionary<Keys, TValue> _dictionary = new Dictionary<Keys, TValue>();

    public WeakTable()
    {
        WeakEventManager<GCNotifier, EventArgs>.AddHandler(null, nameof(GCNotifier.Collected), 
            (s, e) => CheckReferences());
    }

    public void Add(TKey1 key1, TKey2 key2, TValue value)
    {
        lock (_lockObj)
        {
            _dictionary[new Keys(key1, key2)] = value;
        }
    }

    public bool TryGetValue(TKey1 key1, TKey2 key2, ref TValue value)
    {
        lock (_lockObj)
        {
            var key = new Keys(key1, key2);
            if (_dictionary.ContainsKey(key) == false)
                return false;
            value = _dictionary[key];
            return true;
        }
    }

    public void CheckReferences()
    {
        lock (_lockObj)
        {
            _dictionary = _dictionary
                .Where(x => x.Key.IsAlive())
                .ToDictionary(x => x.Key, x => x.Value);
        }
    }
}