Files
Performance-Fish/Source/PerformanceFish/Collections/FishTable.cs
T
2022-10-24 18:24:02 +02:00

645 lines
17 KiB
C#

// Copyright (c) 2022 bradson
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using RimWorld;
using UnityEngine;
namespace PerformanceFish.Collections;
#pragma warning disable CS8766, CS8767
public class FishTable<TKey, TValue> : IDictionary<TKey, TValue>, IDictionary, IReadOnlyDictionary<TKey, TValue>
{
//private int[] _buckets;
internal Entry[] _entries;
private int _count;
private int _version;
//private unsafe delegate*<TKey, int> _keyGetHashCode = FunctionPointers.GetHashCode<TKey>.Default;
//private unsafe delegate*<TKey, TKey, bool> _keyEquals = FunctionPointers.Equals<TKey>.Default;
//private int _freeList;
//private int _freeCount;
//private KeyCollection _keys;
//private ValueCollection _values;
public ICollection<TKey> Keys => throw new NotImplementedException();
public ICollection<TValue> Values => throw new NotImplementedException();
public int Version
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _version;
}
public int Count
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _count;
}
public bool IsReadOnly => false;
ICollection IDictionary.Keys => throw new NotImplementedException();
ICollection IDictionary.Values => throw new NotImplementedException();
bool IDictionary.IsFixedSize => false;
object ICollection.SyncRoot => this; // matching System.Collections.Generic.Dictionary
bool ICollection.IsSynchronized => false;
IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys => throw new NotImplementedException();
IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values => throw new NotImplementedException();
object? IDictionary.this[object key]
{
get
=> key is TKey tkey
? this[tkey]
: ThrowHelper.ThrowKeyNotFoundException<object, TValue>(key);
set
{
if (key is TKey tkey)
{
if (value is TValue tvalue)
this[tkey] = tvalue;
else if (value is null)
this[tkey] = default;
else
ThrowHelper.ThrowWrongValueTypeArgumentException(value);
}
else
{
ThrowHelper.ThrowWrongKeyTypeArgumentException(key);
}
}
}
public TValue? this[TKey key]
{
get
{
var keyCode = (uint)HashCode.Get(key);
var entry = _entries[FastModulo(keyCode, (uint)_entries.Length)];
while (entry.HashCode != keyCode || !entry.Key.Equals<TKey>(key))
{
if (entry.Next != EntryReference.Empty && entry.Next != null)
entry = entry.Next.Entry;
else
ThrowHelper.ThrowKeyNotFoundException(key);
}
if (entry.Next is null)
ThrowHelper.ThrowKeyNotFoundException(key);
return entry.Value;
}
set
{
ref var reference = ref TryGetReferenceUnsafe(key);
if (!Unsafe.IsNullRef(ref reference))
{
reference = value;
_version++;
return;
}
Add(key, value!);
}
}
public FishTable() : this(0) { }
public FishTable(int minimumCapacity)
=> _entries = new Entry[
minimumCapacity <= 1 ? 1 // Minimum size to not throw on indexing into _entries. Length is not checked beforehand
: Mathf.NextPowerOfTwo(minimumCapacity)]; // System.Collections.Dictionary starts at 3. So wasteful.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static uint FastModulo(uint first, uint second)
=> first & (second - 1);
public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value)
{
var keyCode = (uint)HashCode.Get(key);
var entry = _entries[FastModulo(keyCode, (uint)_entries.Length)];
while (entry.HashCode != keyCode || !entry.Key.Equals<TKey>(key))
{
if (entry.Next != EntryReference.Empty && entry.Next != null)
{
entry = entry.Next.Entry;
}
else
{
value = default;
return false;
}
}
value = entry.Value;
return entry.Next != null;
}
public TValue? TryGetValue(TKey key)
{
var keyCode = (uint)HashCode.Get(key);
var entry = _entries[FastModulo(keyCode, (uint)_entries.Length)];
while (entry.HashCode != keyCode || !entry.Key.Equals<TKey>(key))
{
if (entry.Next != EntryReference.Empty && entry.Next != null)
entry = entry.Next.Entry;
else
return default;
}
return entry.Value;
}
public ref TValue GetReference(TKey key)
{
var keyCode = (uint)HashCode.Get(key);
ref var entry = ref _entries[FastModulo(keyCode, (uint)_entries.Length)];
while (entry.HashCode != keyCode || !entry.Key.Equals<TKey>(key))
{
if (entry.Next != EntryReference.Empty && entry.Next != null)
entry = ref entry.Next.Entry;
else
ThrowHelper.ThrowKeyNotFoundException(key);
}
if (entry.Next == null)
ThrowHelper.ThrowKeyNotFoundException(key);
return ref entry.Value;
}
/// <summary>
/// Returns a reference to the value field of an entry if the key exists and an Unsafe.NullRef{TValue} if not.
/// Must be checked with Unsafe.IsNullRef before assigning to a regular var or field without ref.
/// </summary>
/// <param name="key">The key</param>
/// <returns>ref TValue or Unsafe.NullRef{TValue}</returns>
public ref TValue TryGetReferenceUnsafe(TKey key)
{
var keyCode = (uint)HashCode.Get(key);
ref var entry = ref _entries[FastModulo(keyCode, (uint)_entries.Length)];
while (entry.HashCode != keyCode || !entry.Key.Equals<TKey>(key))
{
if (entry.Next != EntryReference.Empty && entry.Next != null)
entry = ref entry.Next.Entry;
else
return ref Unsafe.NullRef<TValue>()!;
}
return ref entry.Next == null
? ref Unsafe.NullRef<TValue>()
: ref entry.Value;
}
public bool ContainsKey(TKey key)
{
var keyCode = (uint)HashCode.Get(key);
var entry = _entries[FastModulo(keyCode, (uint)_entries.Length)];
while (entry.HashCode != keyCode || !entry.Key.Equals<TKey>(key))
{
if (entry.Next != EntryReference.Empty && entry.Next != null)
entry = entry.Next.Entry;
else
return false;
}
return entry.Next != null;
}
public void Add(TKey key, TValue value)
{
Guard.IsNotNull(key);
if (_count == _entries.Length) // throwing DuplicateKeyExceptions happens later, so Expanding occurs based on expected
Expand(); // behaviour instead of matching actual results like in System.Collections.Generic.
var keyCode = (uint)HashCode.Get(key);
ref var entry = ref _entries[FastModulo(keyCode, (uint)_entries.Length)];
while (entry.Next != null)
{
if (entry.HashCode == keyCode && entry.Key.Equals<TKey>(key))
ThrowHelper.ThrowAddingDuplicateWithKeyArgumentException(key);
if (entry.Next == EntryReference.Empty)
break;
entry = ref entry.Next.Entry;
}
if (entry.Next == null) // means the entry is empty
entry = new(keyCode, key, value);
else // if (entry.Next == _freeSlot) // occupied entry with an empty slot in entry.Next
entry.Next = new(keyCode, key, value);
// entry.Next != _freeSlot would be a threading issue, as this was just checked in the loop. Unhandled.
_count++;
_version++;
}
private void Expand()
{
var newLength = Mathf.NextPowerOfTwo(_entries.Length + 1);
var newArray = new Entry[newLength];
for (var i = 0; i < _entries.Length; i++)
{
var entry = _entries[i];
if (entry.Next is null)
continue;
BeforeAdding:
ref var newEntry = ref newArray[FastModulo(entry.HashCode, (uint)newLength)];
while (newEntry.Next != null)
{
if (newEntry.HashCode == entry.HashCode && newEntry.Key.Equals<TKey>(entry.Key))
ThrowHelper.ThrowAddingDuplicateWithKeyArgumentException(entry.Key);
if (newEntry.Next == EntryReference.Empty)
break;
newEntry = ref newEntry.Next.Entry;
}
var nextEntry = entry.Next;
entry.Next = EntryReference.Empty;
if (newEntry.Next == null)
newEntry = entry;
else
newEntry.Next = new(entry);
if (nextEntry != EntryReference.Empty)
{
entry = nextEntry.Entry;
goto BeforeAdding;
}
}
_entries = newArray;
}
private ref Entry TryFindEntry(TKey key)
{
var keyCode = (uint)HashCode.Get(key);
ref var entry = ref _entries[FastModulo(keyCode, (uint)_entries.Length)];
while (entry.HashCode != keyCode || !entry.Key.Equals<TKey>(key))
{
if (entry.Next != EntryReference.Empty && entry.Next != null)
entry = ref entry.Next.Entry;
else
return ref Unsafe.NullRef<Entry>();
}
return ref entry;
}
private void RemoveEntry(ref Entry entry)
{
entry = entry.Next != EntryReference.Empty && entry.Next != null
? entry.Next.Entry
: default;
_count--;
_version++;
}
public bool Remove(TKey key)
{
ref var entry = ref TryFindEntry(key);
if (Unsafe.IsNullRef(ref entry))
return false;
RemoveEntry(ref entry);
return true;
}
public void Add(KeyValuePair<TKey, TValue> item) => Add(item.Key, item.Value);
public void Clear()
{
Array.Clear(_entries, 0, _entries.Length);
_count = 0;
_version++;
}
public bool Contains(KeyValuePair<TKey, TValue> item)
=> TryGetValue(item.Key, out var value)
&& value.Equals<TValue>(item.Value);
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
{
var length = _entries.Length;
for (var i = 0; i < length; i++)
{
var entry = _entries[i];
if (entry.Next == null)
continue;
array[arrayIndex] = new(entry.Key, entry.Value);
arrayIndex++;
while (entry.Next != EntryReference.Empty)
{
entry = entry.Next.Entry;
array[arrayIndex] = new(entry.Key, entry.Value);
arrayIndex++;
}
}
}
public bool Remove(KeyValuePair<TKey, TValue> item)
{
ref var entry = ref TryFindEntry(item.Key);
if (Unsafe.IsNullRef(ref entry)
|| !entry.Value.Equals<TValue>(item.Value))
{
return false;
}
RemoveEntry(ref entry);
return true;
}
public Enumerator GetEnumerator()
=> new(this, Enumerator.KEY_VALUE_PAIR);
IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator()
=> new Enumerator(this, Enumerator.KEY_VALUE_PAIR);
IEnumerator IEnumerable.GetEnumerator()
=> new Enumerator(this, Enumerator.KEY_VALUE_PAIR);
bool IDictionary.Contains(object key)
{
Guard.IsNotNull(key);
return key is TKey tkey
&& ContainsKey(tkey);
}
void IDictionary.Add(object key, object value) => throw new NotImplementedException();
IDictionaryEnumerator IDictionary.GetEnumerator()
=> new Enumerator(this, Enumerator.DICT_ENTRY);
void IDictionary.Remove(object key)
{
Guard.IsNotNull(key);
if (key is TKey tkey)
Remove(tkey);
}
void ICollection.CopyTo(Array array, int index) => throw new NotImplementedException();
internal struct Entry
{
public uint HashCode;
public TKey Key;
public EntryReference Next;
public TValue Value;
internal Entry(uint hashCode, TKey key, TValue value)
{
HashCode = hashCode;
Key = key;
Value = value;
Next = EntryReference.Empty;
}
}
internal class EntryReference
{
public static readonly EntryReference Empty = new();
public Entry Entry;
public EntryReference(uint hashCode, TKey key, TValue value) => Entry = new(hashCode, key, value);
public EntryReference(Entry entry) => Entry = entry;
private EntryReference() { }
}
public struct Enumerator : IEnumerator<KeyValuePair<TKey, TValue>>, IDictionaryEnumerator
{
private readonly FishTable<TKey, TValue> _dictionary;
private readonly int _version;
private int _index;
private EntryReference _next;
private KeyValuePair<TKey, TValue> _current;
private readonly int _getEnumeratorRetType; // What should Enumerator.Current return?
internal const int DICT_ENTRY = 1;
internal const int KEY_VALUE_PAIR = 2;
internal Enumerator(FishTable<TKey, TValue> dictionary, int getEnumeratorRetType)
{
_dictionary = dictionary;
_version = dictionary._version;
_index = 0;
_getEnumeratorRetType = getEnumeratorRetType;
_current = default;
_next = EntryReference.Empty;
}
public bool MoveNext()
{
if (_version != _dictionary._version)
ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion();
if (_next != EntryReference.Empty)
{
_current = new(_next.Entry.Key, _next.Entry.Value);
_next = _next.Entry.Next;
return true;
}
// Use unsigned comparison since we set index to dictionary.count+1 when the enumeration ends.
// dictionary.count+1 could be negative if dictionary.count is int.MaxValue
while ((uint)_index < (uint)_dictionary._entries.Length)
{
ref var entry = ref _dictionary._entries[_index++];
if (entry.Next != null)
{
_current = new KeyValuePair<TKey, TValue>(entry.Key, entry.Value);
_next = entry.Next;
return true;
}
}
_index = _dictionary._entries.Length + 1;
_current = default;
return false;
}
public KeyValuePair<TKey, TValue> Current => _current;
public void Dispose() { }
object? IEnumerator.Current
{
get
{
if (_index == 0 || (_index == _dictionary._entries.Length + 1))
ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen();
return _getEnumeratorRetType == DICT_ENTRY
? new DictionaryEntry(_current.Key, _current.Value)
: new KeyValuePair<TKey, TValue>(_current.Key, _current.Value);
}
}
void IEnumerator.Reset()
{
if (_version != _dictionary._version)
ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion();
_index = 0;
_current = default;
}
DictionaryEntry IDictionaryEnumerator.Entry
{
get
{
if (_index == 0 || (_index == _dictionary._entries.Length + 1))
ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen();
return new DictionaryEntry(_current.Key, _current.Value);
}
}
object? IDictionaryEnumerator.Key
{
get
{
if (_index == 0 || (_index == _dictionary._entries.Length + 1))
ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen();
return _current.Key;
}
}
object? IDictionaryEnumerator.Value
{
get
{
if (_index == 0 || (_index == _dictionary._entries.Length + 1))
ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen();
return _current.Value;
}
}
}
private static class ThrowHelper
{
[DoesNotReturn]
internal static void ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion()
=> throw new InvalidOperationException("Collection was modified; enumeration operation may not execute.");
[DoesNotReturn]
internal static void ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen()
=> throw new InvalidOperationException("Enumeration has either not started or has already finished.");
[DoesNotReturn]
internal static void ThrowKeyNotFoundException<T>(T key)
{
Guard.IsNotNull(key);
ThrowKeyNotFoundException((object)key);
}
[DoesNotReturn]
internal static V ThrowKeyNotFoundException<T, V>(T key)
{
Guard.IsNotNull(key);
ThrowKeyNotFoundException((object)key);
return default;
}
[DoesNotReturn]
internal static void ThrowKeyNotFoundException(object key)
=> throw new KeyNotFoundException(
string.Format("The given key '{0}' was not present in the dictionary.", key));
[DoesNotReturn]
internal static void ThrowWrongValueTypeArgumentException(object value)
=> throw new ArgumentException(
string.Format("The value \"{0}\" is not of type \"{1}\" and cannot be used in this generic collection.",
value, typeof(TValue)),
nameof(value));
[DoesNotReturn]
internal static void ThrowWrongKeyTypeArgumentException(object? key)
{
Guard.IsNotNull(key);
throw new ArgumentException(
string.Format("The value \"{0}\" is not of type \"{1}\" and cannot be used in this generic collection.",
key, typeof(TKey)),
nameof(key));
}
[DoesNotReturn]
internal static void ThrowAddingDuplicateWithKeyArgumentException<T>(T key)
{
Guard.IsNotNull(key);
ThrowAddingDuplicateWithKeyArgumentException((object)key);
}
[DoesNotReturn]
internal static void ThrowAddingDuplicateWithKeyArgumentException(object key)
=> throw new ArgumentException(
string.Format("An item with the same key has already been added. Key: {0}", key));
}
}
public static class Extensions
{
public static ref TValue TryGetReferenceUnsafe<TKey, TValue>(this FishTable<TKey, TValue> fishTable, ref TKey key)
where TKey : struct, IEquatable<TKey>
{
var keyCode = (uint)HashCode.Get(ref key);
ref var entry = ref fishTable._entries[FishTable<TKey, TValue>.FastModulo(keyCode, (uint)fishTable._entries.Length)];
while (entry.HashCode != keyCode || !entry.Key.Equals(ref key))
{
if (entry.Next != FishTable<TKey, TValue>.EntryReference.Empty && entry.Next != null)
entry = ref entry.Next.Entry;
else
return ref Unsafe.NullRef<TValue>()!;
}
return ref entry.Next == null
? ref Unsafe.NullRef<TValue>()
: ref entry.Value;
}
}
#pragma warning restore CS8766, CS8767