Parsing tokens from a string with string.split and a dictionary causes allocations.
These allocations can be avoided by handling the token string as a span and splitting the tokens and using them for dictionary look-ups.
Proposed API
A dictionary based on Dictionary<TKey,TValue> but with additional functions for first class ReadOnlySpan support.
[DebuggerDisplay("Count = {Count}")]
public class MemoryDictionary<TKey, TValue> : IDictionary<ReadOnlyMemory<TKey>, TValue>, IDictionary, IReadOnlyDictionary<ReadOnlyMemory<TKey>, TValue>
where TKey : notnull, IEquatable<TKey>
{
private int[] _buckets;
private Entry[] _entries;
private int _count;
private int _freeList;
private int _freeCount;
private int _version;
private IMemoryEqualityComparer<TKey> _comparer;
private KeyCollection _keys;
private ValueCollection _values;
private const int StartOfFreeList = -3;
public MemoryDictionary();
public MemoryDictionary(int capacity);
public MemoryDictionary(IMemoryEqualityComparer<TKey> comparer);
public MemoryDictionary(int capacity, IMemoryEqualityComparer<TKey> comparer);
public MemoryDictionary(IDictionary<ReadOnlyMemory<TKey>, TValue> dictionary);
public MemoryDictionary(IDictionary<ReadOnlyMemory<TKey>, TValue> dictionary, IMemoryEqualityComparer<TKey> comparer);
public MemoryDictionary(IEnumerable<KeyValuePair<ReadOnlyMemory<TKey>, TValue>> collection);
public MemoryDictionary(IEnumerable<KeyValuePair<ReadOnlyMemory<TKey>, TValue>> collection, IMemoryEqualityComparer<TKey> comparer);
public int Count { get; }
public KeyCollection Keys { get; }
public ValueCollection Values { get; }
public IMemoryEqualityComparer<TKey> Comparer { get; }
public TValue this[ReadOnlyMemory<TKey> key] { get; set; }
public TValue Get(ReadOnlyMemory<TKey> key);
public TValue GetWithSpan(ReadOnlySpan<TKey> key);
public bool TryGetValue(ReadOnlyMemory<TKey> key, out TValue value);
public bool TryGetValueWithSpan(ReadOnlySpan<TKey> key, [MaybeNullWhen(false)] out TValue value);
public Enumerator GetEnumerator();
public void Add(ReadOnlyMemory<TKey> key, TValue value);
public bool TryAdd(ReadOnlyMemory<TKey> key, TValue value);
public bool Remove(ReadOnlyMemory<TKey> key);
public bool Remove(ReadOnlyMemory<TKey> key, out TValue value) => RemoveWithSpan(key.Span, out value);
public bool RemoveWithSpan(ReadOnlySpan<TKey> key);
public bool RemoveWithSpan(ReadOnlySpan<TKey> key, [MaybeNullWhen(false)] out TValue value);
public void Clear();
public bool ContainsKey(ReadOnlyMemory<TKey> key);
public bool ContainsValue(TValue value);
private int FindEntry(ReadOnlyMemory<TKey> key);
private int FindEntry(ReadOnlySpan<TKey> key);
private bool TryInsert(ReadOnlyMemory<TKey> key, TValue value, InsertionBehavior behavior);
private void CopyTo(KeyValuePair<ReadOnlyMemory<TKey>, TValue>[] array, int index);
private int Initialize(int capacity);
public int EnsureCapacity(int capacity);
private void Resize();
private void Resize(int newSize, bool forceNewHashCodes);
public void TrimExcess();
public void TrimExcess(int capacity);
private static bool IsCompatibleKey(object key);
ICollection<ReadOnlyMemory<TKey>> IDictionary<ReadOnlyMemory<TKey>, TValue>.Keys { get; }
IEnumerable<ReadOnlyMemory<TKey>> IReadOnlyDictionary<ReadOnlyMemory<TKey>, TValue>.Keys { get; }
ICollection IDictionary.Keys { get; }
ICollection<TValue> IDictionary<ReadOnlyMemory<TKey>, TValue>.Values { get; }
IEnumerable<TValue> IReadOnlyDictionary<ReadOnlyMemory<TKey>, TValue>.Values { get; }
ICollection IDictionary.Values { get; }
object IDictionary.this[object key] { get; set; }
IEnumerator IEnumerable.GetEnumerator();
IDictionaryEnumerator IDictionary.GetEnumerator();
void ICollection<KeyValuePair<ReadOnlyMemory<TKey>, TValue>>.Add(KeyValuePair<ReadOnlyMemory<TKey>, TValue> keyValuePair);
void IDictionary.Add(object key, object value);
bool ICollection<KeyValuePair<ReadOnlyMemory<TKey>, TValue>>.Contains(KeyValuePair<ReadOnlyMemory<TKey>, TValue> keyValuePair);
bool IDictionary.Contains(object key);
bool ICollection<KeyValuePair<ReadOnlyMemory<TKey>, TValue>>.Remove(KeyValuePair<ReadOnlyMemory<TKey>, TValue> keyValuePair);
void IDictionary.Remove(object key);
IEnumerator<KeyValuePair<ReadOnlyMemory<TKey>, TValue>> IEnumerable<KeyValuePair<ReadOnlyMemory<TKey>, TValue>>.GetEnumerator();
bool ICollection<KeyValuePair<ReadOnlyMemory<TKey>, TValue>>.IsReadOnly { get; }
bool IDictionary.IsReadOnly { get; }
bool IDictionary.IsFixedSize { get; }
void ICollection.CopyTo(Array array, int index);
void ICollection<KeyValuePair<ReadOnlyMemory<TKey>, TValue>>.CopyTo(KeyValuePair<ReadOnlyMemory<TKey>, TValue>[] array, int index);
bool ICollection.IsSynchronized { get; }
object ICollection.SyncRoot { get; }
private enum InsertionBehavior : byte
{
None = 0,
OverwriteExisting = 1,
ThrowOnExisting = 2
}
private struct Entry
{
public int next;
public uint hashCode;
public ReadOnlyMemory<TKey> key;
public TValue value;
}
public struct Enumerator : IEnumerator<KeyValuePair<ReadOnlyMemory<TKey>, TValue>>,
IDictionaryEnumerator
{
private readonly MemoryDictionary<TKey, TValue> _dictionary;
private readonly int _version;
private int _index;
private KeyValuePair<ReadOnlyMemory<TKey>, TValue> _current;
private readonly int _getEnumeratorRetType; // What should Enumerator.Current return
internal const int DictEntry = 1;
internal const int KeyValuePair = 2;
internal Enumerator(MemoryDictionary<TKey, TValue> dictionary, int getEnumeratorRetType);
public bool MoveNext();
public KeyValuePair<ReadOnlyMemory<TKey>, TValue> Current { get; }
public void Dispose();
object IEnumerator.Current { get; }
void IEnumerator.Reset();
DictionaryEntry IDictionaryEnumerator.Entry { get; }
object IDictionaryEnumerator.Key { get; }
object IDictionaryEnumerator.Value { get; }
}
[DebuggerDisplay("Count = {Count}")]
public sealed class KeyCollection : ICollection<ReadOnlyMemory<TKey>>, ICollection, IReadOnlyCollection<ReadOnlyMemory<TKey>>
{
private MemoryDictionary<TKey, TValue> _dictionary;
public KeyCollection(MemoryDictionary<TKey, TValue> dictionary);
public Enumerator GetEnumerator();
public void CopyTo(ReadOnlyMemory<TKey>[] array, int index);
public int Count { get; }
bool ICollection<ReadOnlyMemory<TKey>>.IsReadOnly { get; }
void ICollection<ReadOnlyMemory<TKey>>.Add(ReadOnlyMemory<TKey> item);
void ICollection<ReadOnlyMemory<TKey>>.Clear();
bool ICollection<ReadOnlyMemory<TKey>>.Contains(ReadOnlyMemory<TKey> item);
bool ICollection<ReadOnlyMemory<TKey>>.Remove(ReadOnlyMemory<TKey> item);
IEnumerator<ReadOnlyMemory<TKey>> IEnumerable<ReadOnlyMemory<TKey>>.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator();
void ICollection.CopyTo(Array array, int index);
bool ICollection.IsSynchronized { get; }
object ICollection.SyncRoot { get; }
public struct Enumerator : IEnumerator<ReadOnlyMemory<TKey>>, IEnumerator
{
private readonly MemoryDictionary<TKey, TValue> _dictionary;
private int _index;
private readonly int _version;
private ReadOnlyMemory<TKey> _currentKey;
internal Enumerator(MemoryDictionary<TKey, TValue> dictionary);
public void Dispose();
public bool MoveNext();
public ReadOnlyMemory<TKey> Current { get; }
object IEnumerator.Current { get; }
void IEnumerator.Reset();
}
}
[DebuggerDisplay("Count = {Count}")]
public sealed class ValueCollection : ICollection<TValue>, ICollection, IReadOnlyCollection<TValue>
{
private MemoryDictionary<TKey, TValue> _dictionary;
public ValueCollection(MemoryDictionary<TKey, TValue> dictionary);
public Enumerator GetEnumerator();
public void CopyTo(TValue[] array, int index);
public int Count { get; }
bool ICollection<TValue>.IsReadOnly { get; }
void ICollection<TValue>.Add(TValue item);
bool ICollection<TValue>.Remove(TValue item);
void ICollection<TValue>.Clear();
bool ICollection<TValue>.Contains(TValue item);
IEnumerator<TValue> IEnumerable<TValue>.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator();
void ICollection.CopyTo(Array array, int index);
bool ICollection.IsSynchronized { get; }
object ICollection.SyncRoot { get; }
public struct Enumerator : IEnumerator<TValue>, IEnumerator
{
private readonly MemoryDictionary<TKey, TValue> _dictionary;
private int _index;
private readonly int _version;
private TValue _currentValue;
internal Enumerator(MemoryDictionary<TKey, TValue> dictionary);
public void Dispose();
public bool MoveNext();
public TValue Current { get; }
object IEnumerator.Current { get; }
void IEnumerator.Reset();
}
}
}
For equality comparing keys with spans the normal IEqualityComparer has to be extended
public interface IMemoryEqualityComparer<T> : IEqualityComparer<ReadOnlyMemory<T>>
{
bool Equals(ReadOnlyMemory<T> x, ReadOnlySpan<T> y);
int GetHashCode(ReadOnlySpan<T> span);
}
The default MemoryEqualityComparer would look like this
public class MemoryEqualityComparer<T> : IMemoryEqualityComparer<T>
where T : IEquatable<T>
{
public static MemoryEqualityComparer<T> Default { get; }
private MemoryEqualityComparer();
public bool Equals(ReadOnlyMemory<T> x, ReadOnlyMemory<T> y);
public bool Equals(ReadOnlyMemory<T> x, ReadOnlySpan<T> y);
public int GetHashCode(ReadOnlyMemory<T> memory);
public int GetHashCode(ReadOnlySpan<T> span);
}
example of usage:
[Flags]
public enum ScopeField
{
Identity = 1 << 0,
IdentityEmail = Identity | (1 << 1),
IdentityMemberships = Identity | (1 << 2)
}
private readonly static Dictionary<ReadOnlyMemory<char>, ScopeField> _mapping = new Dictionary<ReadOnlyMemory<char>, ScopeField>()
{
["identity".AsMemory()] = ScopeField.Identity,
["identity[email]".AsMemory()] = ScopeField.IdentityEmail,
["identity.memberships".AsMemory()] = ScopeField.IdentityMemberships
};
public static ScopeField ParseScopeStringWithSpan(string scopeString)
{
ScopeField scopeValue = 0;
var scopeSpan = scopeString.AsSpan();
while (scopeSpan.Length != 0)
{
int sliceIndex = scopeSpan.IndexOf(' ');
if (sliceIndex != -1)
{
if (sliceIndex == 0)
{
scopeSpan = scopeSpan.Slice(1);
continue;
}
scopeValue |= _mapping.GetWithSpan(scopeSpan.Slice(0, sliceIndex));
scopeSpan = scopeSpan.Slice(sliceIndex + 1);
}
else
{
scopeValue |= _mapping.GetWithSpan(scopeSpan);
break;
}
}
return scopeValue;
}
Parsing tokens from a string with string.split and a dictionary causes allocations.
These allocations can be avoided by handling the token string as a span and splitting the tokens and using them for dictionary look-ups.
Proposed API
A dictionary based on
Dictionary<TKey,TValue>but with additional functions for first class ReadOnlySpan support.For equality comparing keys with spans the normal IEqualityComparer has to be extended
The default MemoryEqualityComparer would look like this
example of usage: