I have created my own version of it. It is a bit more sophisticated. The documentation should explain some of the functionality. In essence it allows a reasonable way to handle 2 keys for the same value, auto-merges entries, is presumably thread-safe (untested), allows mapping keys together, and handles deleting entries, all the while having the functionality of dictionaries at its base. When adding an entry, but one of its keys already exists, it will just add the key and overwrite the value. It's quite distinct logically from other forms of collections, so these are the most I was able to implement.
Some structure with key pairs didn't seem fitting, given that I need this to arbitrarily add a second key as needed to existing entries, and given the merging functionality. I also took regard for the situation where one uses the same types for both keys, but also for situations where they don't.
/// <summary> A collection that internally uses a list (which in turn internally uses an array), and two dictionaries for the index. /// This allows operating it based on two keys and provides means to (automatically) map keys to each other. /// The indexing of the internal list is treated specially. In order to not infringe on the validity of the dictionaries' references to the indexes, /// they are kept identical. Removing is handled by setting the entries to 'null', and once a new item is added, they are overwritten. </summary> /// <typeparam name="TKey1"> The first key. </typeparam> /// <typeparam name="TKey2"> The second key. </typeparam> /// <typeparam name="T"> The stored value type. </typeparam> public class TwoKeyDictionary<TKey1, TKey2, T> : IEnumerable<TwoKeyDictionaryEntry<TKey1, TKey2, T>>, IReadOnlyCollection<TwoKeyDictionaryEntry<TKey1, TKey2, T>> { private readonly Dictionary<TKey1, int> _keys01 = new Dictionary<TKey1, int> (); private readonly Dictionary<TKey2, int> _keys02 = new Dictionary<TKey2, int> (); private readonly List<TwoKeyDictionaryEntry<TKey1, TKey2, T>> _items = new List<TwoKeyDictionaryEntry<TKey1, TKey2, T>> (); private int _freeIndex = 0; // The index of the first free slot. private int _freeCount = 0; // Free before the last value. private readonly object _lock = new object (); public TwoKeyDictionary () { } /// <summary> Adds an item. </summary> public bool Add (TKey1 key, T value) { return AddByKey1 (key, value); } /// <summary> Adds an item. </summary> public bool Add (TKey2 key, T value) { return AddByKey2 (key, value); } /// <summary> Adds an item. </summary> public bool AddByKey1 (TKey1 key, T value) { lock (_lock) { return AddByKey1Internal (key, value); } } /// <summary> Adds an item. </summary> public bool AddByKey2 (TKey2 key, T value) { lock (_lock) { return AddByKey2Internal (key, value); } } /// <summary> Adds an item with two keys. If either key already exists, it will map the other key to it. The value will only be overwritten if it's 'null'. </summary> public bool Add (TKey1 key1, TKey2 key2, T value) { return Add (key1, key2, value, false); } /// <summary> Adds an item with two keys. If either key already exists, it will map the other key to it. The value will only be overwritten if it's 'null'. /// This may also define how the key is mapped, if occurring. </summary> public bool Add (TKey1 key1, TKey2 key2, T value, bool mapToKey2) { lock (_lock) { return AddInternal (key1, key2, value, mapToKey2); } } /// <summary> Maps both keys together. If either key exists, it will add the other one to it. If both exist, it will merge the entries and delete the other. /// By default this will map to key1. </summary> public bool Map (TKey1 key1, TKey2 key2) { return MapToKey1 (key1, key2); } /// <summary> Maps both keys together. If either key exists, it will add the other one to it. If both exist, it will merge the entries and delete the one with key2. </summary> public bool MapToKey1 (TKey1 key1, TKey2 key2) { lock (_lock) { return MapToKey1Internal (key1, key2); } } /// <summary> Maps both keys together. If either key exists, it will add the other one to it. If both exist, it will merge the entries and delete the one with key1. </summary> public bool MapToKey2 (TKey1 key1, TKey2 key2) { lock (_lock) { return MapToKey2Internal (key1, key2); } } /// <summary> Removes an entry based on key1. If there is a key2 mapped to it, it will be removed as well. </summary> public bool Remove (TKey1 key) { return RemoveByKey1 (key); } /// <summary> Removes an entry based on key2. If there is a key1 mapped to it, it will be removed as well. </summary> public bool Remove (TKey2 key) { return RemoveByKey2 (key); } /// <summary> Removes an entry based on key1. If there is a key2 mapped to it, it will be removed as well. </summary> public bool RemoveByKey1 (TKey1 key) { lock (_lock) { return RemoveByKey1Internal (key); } } /// <summary> Removes an entry based on key2. If there is a key1 mapped to it, it will be removed as well. </summary> public bool RemoveByKey2 (TKey2 key) { lock (_lock) { return RemoveByKey2Internal (key); } } /// <summary> Removes an entry based on both, key1 and key2. Any entries related to either keys will be removed. </summary> public bool Remove (TKey1 key1, TKey2 key2) { lock (_lock) { return RemoveByKey1Internal (key1) | RemoveByKey2Internal (key2); } } /// <summary> Tries to return a value based on key1. </summary> public bool TryGetValue (TKey1 key, out T value) { return TryGetValueByKey1 (key, out value); } /// <summary> Tries to return a value based on key2. </summary> public bool TryGetValue (TKey2 key, out T value) { return TryGetValueByKey2 (key, out value); } /// <summary> Tries to return a value based on key1. </summary> public bool TryGetValueByKey1 (TKey1 key, out T value) { if (key == null) { value = default; return false; } if (_keys01.TryGetValue (key, out int index)) { TwoKeyDictionaryEntry<TKey1, TKey2, T> entry = _items[index]; if (entry != null) { value = entry.Value; return true; } } value = default; return false; } /// <summary> Tries to return a value based on key2. </summary> public bool TryGetValueByKey2 (TKey2 key, out T value) { if (key == null) { value = default; return false; } if (_keys02.TryGetValue (key, out int index)) { TwoKeyDictionaryEntry<TKey1, TKey2, T> entry = _items[index]; if (entry != null) { value = entry.Value; return true; } } value = default; return false; } /// <summary> Tries to return a value based on key1 or key2. Prioritizes key1. </summary> public bool TryGetValue (TKey1 key1, TKey2 key2, out T value) { return TryGetValue (key1, key2, false, out value); } /// <summary> Tries to return a value based on key1 or key2. </summary> public bool TryGetValue (TKey1 key1, TKey2 key2, bool prioritizeKey2, out T value) { return prioritizeKey2 ? TryGetValue (key1, out value) || TryGetValue (key2, out value) : TryGetValue (key2, out value) || TryGetValue (key1, out value); } /// <summary> Returns 'true' if they key and the entry still exists. The stored value itself may still be 'null' regardless. </summary> public bool ContainsKey (TKey1 key) { return ContainsKey1 (key); } /// <summary> Returns 'true' if they key and the entry still exists. The stored value itself may still be 'null' regardless. </summary> public bool ContainsKey (TKey2 key) { return ContainsKey2 (key); } /// <summary> Returns 'true' if they key and the entry still exists. The stored value itself may still be 'null' regardless. </summary> public bool ContainsKey1 (TKey1 key) { if (key == null) return false; if (_keys01.TryGetValue (key, out int index)) return _items[index] != null; else return false; } /// <summary> Returns 'true' if they key and the entry still exists. The stored value itself may still be 'null' regardless. </summary> public bool ContainsKey2 (TKey2 key) { if (key == null) return false; if (_keys02.TryGetValue (key, out int index)) return _items[index] != null; else return false; } /// <summary> Returns 'true' if they key and the entry still exists. The stored value itself may still be 'null' regardless. </summary> public bool ContainsKey (TKey1 key1, TKey2 key2) { return ContainsKey1 (key1) || ContainsKey2 (key2); } #region Internal // Returns true if this wasn't the last position. private bool GetFreeIndex (bool apply, out int index) { if (_freeCount == 0) { index = _items.Count; return false; } else { index = _freeIndex; if (apply) { // We must find the next free slot. int freeIndex = _freeIndex + 1; int count = _items.Count; while (freeIndex < count && _items[freeIndex] != null) { freeIndex++; } if (freeIndex == count) _freeCount = 0; else Interlocked.Decrement (ref _freeCount); _freeIndex = freeIndex; } return true; } } private bool MapToKey1Internal (TKey1 key1, TKey2 key2) { if (key1 == null || key2 == null) return false; bool s1 = _keys01.TryGetValue (key1, out int index1); bool s2 = _keys02.TryGetValue (key2, out int index2); if (s1 && s2) { TwoKeyDictionaryEntry<TKey1, TKey2, T> e1 = _items[index1]; TwoKeyDictionaryEntry<TKey1, TKey2, T> e2 = _items[index2]; RemoveByKey2Internal (key2); e1.Key2 = key2; if (e1.Value == null) e1.Value = e2.Value; return true; } else if (s1) { _items[index1].Key2 = key2; _keys02.Add (key2, index1); return true; } else if (s2) { _items[index2].Key1 = key1; _keys01.Add (key1, index2); return true; } else return false; } private bool MapToKey2Internal (TKey1 key1, TKey2 key2) { if (key1 == null || key2 == null) return false; bool s1 = _keys01.TryGetValue (key1, out int index1); bool s2 = _keys02.TryGetValue (key2, out int index2); if (s1 && s2) { TwoKeyDictionaryEntry<TKey1, TKey2, T> e1 = _items[index1]; TwoKeyDictionaryEntry<TKey1, TKey2, T> e2 = _items[index2]; RemoveByKey1Internal (key1); e2.Key1 = key1; if (e2.Value == null) e2.Value = e1.Value; return true; } else if (s1) { _items[index1].Key2 = key2; return true; } else if (s2) { _items[index2].Key1 = key1; return true; } else return false; } private bool AddByKey1Internal (TKey1 key, T value) { if (key == null) return false; if (_keys01.TryGetValue (key, out int index)) { TwoKeyDictionaryEntry<TKey1, TKey2, T> entry = _items[index]; if (entry != null) { entry.Value = value; return true; } else { _keys01.Remove (key); return AddByKey1Internal (key, value); } } else { TwoKeyDictionaryEntry<TKey1, TKey2, T> item = new TwoKeyDictionaryEntry<TKey1, TKey2, T> (key, default, value); if (GetFreeIndex (true, out int freeIndex)) { _items[freeIndex] = item; } else { _items.Add (item); } _keys01.Add (key, freeIndex); return true; } } private bool AddByKey2Internal (TKey2 key, T value) { if (key == null) return false; if (_keys02.TryGetValue (key, out int index)) { TwoKeyDictionaryEntry<TKey1, TKey2, T> entry = _items[index]; if (entry != null) { entry.Value = value; return true; } else { _keys02.Remove (key); return AddByKey2Internal (key, value); } } else { TwoKeyDictionaryEntry<TKey1, TKey2, T> item = new TwoKeyDictionaryEntry<TKey1, TKey2, T> (default, key, value); if (GetFreeIndex (true, out int freeIndex)) { _items[freeIndex] = item; } else { _items.Add (item); } _keys02.Add (key, freeIndex); return true; } } private bool AddInternal (TKey1 key1, TKey2 key2, T value, bool mapToKey2) { if (key1 == null) return AddByKey2Internal (key2, value); else if (key2 == null) return AddByKey1Internal (key1, value); bool hasKey1 = _keys01.TryGetValue (key1, out int index1); bool hasKey2 = _keys02.TryGetValue (key2, out int index2); if (hasKey1 && hasKey2) { // We have 2 different entries' keys that point to the same value. Merge them to one key, remove the other. if (mapToKey2) { if (MapToKey2Internal (key1, key2)) { _items[index2].Value = value; } } else { if (MapToKey1Internal (key1, key2)) { _items[index1].Value = value; } } } else if (hasKey1) { TwoKeyDictionaryEntry<TKey1, TKey2, T> entry = _items[index1]; entry.Key2 = key2; entry.Value = value; } else if (hasKey2) { TwoKeyDictionaryEntry<TKey1, TKey2, T> entry = _items[index2]; entry.Key1 = key1; entry.Value = value; } else { _items.Add (new TwoKeyDictionaryEntry<TKey1, TKey2, T> (key1, key2, value)); } return true; } private bool RemoveByKey1Internal (TKey1 key) { if (key == null) return false; if (_keys01.TryGetValue (key, out int index)) { TwoKeyDictionaryEntry<TKey1, TKey2, T> entry = _items[index]; if (entry != null) { _keys01.Remove (key); if (entry.Key2 != null) _keys02.Remove (entry.Key2); if (index == _items.Count - 1) { _items.RemoveAt (index); } else { _items[index] = null; _freeIndex = _freeCount > 0 ? Math.Min (_freeIndex, index) : index; Interlocked.Increment (ref _freeCount); } return true; } else { _keys01.Remove (key); } } return false; } private bool RemoveByKey2Internal (TKey2 key) { if (key == null) return false; if (_keys02.TryGetValue (key, out int index)) { TwoKeyDictionaryEntry<TKey1, TKey2, T> entry = _items[index]; if (entry != null) { _keys02.Remove (key); if (entry.Key1 != null) _keys01.Remove (entry.Key1); if (index == _items.Count - 1) { _items.RemoveAt (index); } else { _items[index] = null; _freeIndex = _freeCount > 0 ? Math.Min (_freeIndex, index) : index; Interlocked.Increment (ref _freeCount); } return true; } else { _keys02.Remove (key); } } return false; } #endregion #region Interface Implementations public int Count => _items.Count (j => j != null); public IEnumerator<TwoKeyDictionaryEntry<TKey1, TKey2, T>> GetEnumerator () { return _items.Where (j => j != null).GetEnumerator (); } IEnumerator IEnumerable.GetEnumerator () { return _items.Where (j => j != null).GetEnumerator (); } #endregion } /// <summary> The entry class of <see cref="TwoKeyDictionary{TKey1, TKey2, T}"/>, which grants references to the keys in both dictionaries used. </summary> /// <typeparam name="TKey1"> The first key. </typeparam> /// <typeparam name="TKey2"> The second key. </typeparam> /// <typeparam name="T"> The stored value type. </typeparam> public class TwoKeyDictionaryEntry<TKey1, TKey2, T> { public TKey1 Key1 { get; internal set; } public TKey2 Key2 { get; internal set; } public T Value { get; internal set; } internal TwoKeyDictionaryEntry () { } internal TwoKeyDictionaryEntry (TKey1 key1, TKey2 key2, T value) { Key1 = key1; Key2 = key2; Value = value; } public override string ToString () { return $"{Key1?.ToString () ?? "---"} | {Key2?.ToString () ?? "---"} | {Value}"; } }
Dictionary<object, long>not works?