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); } } }