mirror of
https://github.com/chylex/.NET-Community-Toolkit.git
synced 2025-06-05 13:34:10 +02:00

- Remove unused namespaces - Remove unused attributes - Remove unnecessary #nullable enable directives - Sort using directives - Simplify names
405 lines
13 KiB
C#
405 lines
13 KiB
C#
// Licensed to the .NET Foundation under one or more agreements.
|
|
// The .NET Foundation licenses this file to you under the MIT license.
|
|
// See the LICENSE file in the project root for more information.
|
|
|
|
// The DictionarySlim<TKey, TValue> type is originally from CoreFX labs, see
|
|
// the source repository on GitHub at https://github.com/dotnet/corefxlab.
|
|
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Diagnostics.Contracts;
|
|
using System.Runtime.CompilerServices;
|
|
|
|
namespace Microsoft.Collections.Extensions;
|
|
|
|
/// <summary>
|
|
/// A lightweight Dictionary with three principal differences compared to <see cref="Dictionary{TKey, TValue}"/>
|
|
///
|
|
/// 1) It is possible to do "get or add" in a single lookup. For value types, this also saves a copy of the value.
|
|
/// 2) It assumes it is cheap to equate values.
|
|
/// 3) It assumes the keys implement <see cref="IEquatable{TKey}"/> and they are cheap and sufficient.
|
|
/// </summary>
|
|
/// <typeparam name="TKey">The type of keys in the dictionary.</typeparam>
|
|
/// <typeparam name="TValue">The type of values in the dictionary.</typeparam>
|
|
/// <remarks>
|
|
/// 1) This avoids having to do separate lookups (<see cref="Dictionary{TKey, TValue}.TryGetValue(TKey, out TValue)"/>
|
|
/// followed by <see cref="Dictionary{TKey, TValue}.Add(TKey, TValue)"/>.
|
|
/// There is not currently an API exposed to get a value by ref without adding if the key is not present.
|
|
/// 2) This means it can save space by not storing hash codes.
|
|
/// 3) This means it can avoid storing a comparer, and avoid the likely virtual call to a comparer.
|
|
/// </remarks>
|
|
[DebuggerDisplay("Count = {Count}")]
|
|
internal class DictionarySlim<TKey, TValue> : IDictionarySlim<TKey, TValue>
|
|
where TKey : IEquatable<TKey>
|
|
where TValue : class
|
|
{
|
|
/// <summary>
|
|
/// A reusable array of <see cref="Entry"/> items with a single value.
|
|
/// This is used when a new <see cref="DictionarySlim{TKey,TValue}"/> instance is
|
|
/// created, or when one is cleared. The first item being added to this collection
|
|
/// will immediately cause the first resize (see <see cref="AddKey"/> for more info).
|
|
/// </summary>
|
|
private static readonly Entry[] InitialEntries = new Entry[1];
|
|
|
|
/// <summary>
|
|
/// The current number of items stored in the map.
|
|
/// </summary>
|
|
private int count;
|
|
|
|
/// <summary>
|
|
/// The 1-based index for the start of the free list within <see cref="entries"/>.
|
|
/// </summary>
|
|
private int freeList = -1;
|
|
|
|
/// <summary>
|
|
/// The array of 1-based indices for the <see cref="Entry"/> items stored in <see cref="entries"/>.
|
|
/// </summary>
|
|
private int[] buckets;
|
|
|
|
/// <summary>
|
|
/// The array of currently stored key-value pairs (ie. the lists for each hash group).
|
|
/// </summary>
|
|
private Entry[] entries;
|
|
|
|
/// <summary>
|
|
/// A type representing a map entry, ie. a node in a given list.
|
|
/// </summary>
|
|
private struct Entry
|
|
{
|
|
/// <summary>
|
|
/// The key for the value in the current node.
|
|
/// </summary>
|
|
public TKey Key;
|
|
|
|
/// <summary>
|
|
/// The value in the current node, if present.
|
|
/// </summary>
|
|
public TValue? Value;
|
|
|
|
/// <summary>
|
|
/// The 0-based index for the next node in the current list.
|
|
/// </summary>
|
|
public int Next;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="DictionarySlim{TKey, TValue}"/> class.
|
|
/// </summary>
|
|
public DictionarySlim()
|
|
{
|
|
this.buckets = HashHelpers.SizeOneIntArray;
|
|
this.entries = InitialEntries;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public int Count => this.count;
|
|
|
|
/// <inheritdoc/>
|
|
public TValue this[TKey key]
|
|
{
|
|
get
|
|
{
|
|
Entry[] entries = this.entries;
|
|
|
|
for (int i = this.buckets[key.GetHashCode() & (this.buckets.Length - 1)] - 1;
|
|
(uint)i < (uint)entries.Length;
|
|
i = entries[i].Next)
|
|
{
|
|
if (key.Equals(entries[i].Key))
|
|
{
|
|
return entries[i].Value!;
|
|
}
|
|
}
|
|
|
|
ThrowArgumentExceptionForKeyNotFound(key);
|
|
|
|
return default!;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void Clear()
|
|
{
|
|
this.count = 0;
|
|
this.freeList = -1;
|
|
this.buckets = HashHelpers.SizeOneIntArray;
|
|
this.entries = InitialEntries;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether or not the dictionary contains a pair with a specified key.
|
|
/// </summary>
|
|
/// <param name="key">The key to look for.</param>
|
|
/// <returns>Whether or not the key was present in the dictionary.</returns>
|
|
public bool ContainsKey(TKey key)
|
|
{
|
|
Entry[] entries = this.entries;
|
|
|
|
for (int i = this.buckets[key.GetHashCode() & (this.buckets.Length - 1)] - 1;
|
|
(uint)i < (uint)entries.Length;
|
|
i = entries[i].Next)
|
|
{
|
|
if (key.Equals(entries[i].Key))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the value if present for the specified key.
|
|
/// </summary>
|
|
/// <param name="key">The key to look for.</param>
|
|
/// <param name="value">The value found, otherwise <see langword="default"/>.</param>
|
|
/// <returns>Whether or not the key was present.</returns>
|
|
public bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue? value)
|
|
{
|
|
Entry[] entries = this.entries;
|
|
|
|
for (int i = this.buckets[key.GetHashCode() & (this.buckets.Length - 1)] - 1;
|
|
(uint)i < (uint)entries.Length;
|
|
i = entries[i].Next)
|
|
{
|
|
if (key.Equals(entries[i].Key))
|
|
{
|
|
value = entries[i].Value!;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
value = default!;
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public bool TryRemove(TKey key)
|
|
{
|
|
Entry[] entries = this.entries;
|
|
int bucketIndex = key.GetHashCode() & (this.buckets.Length - 1);
|
|
int entryIndex = this.buckets[bucketIndex] - 1;
|
|
int lastIndex = -1;
|
|
|
|
while (entryIndex != -1)
|
|
{
|
|
Entry candidate = entries[entryIndex];
|
|
|
|
if (candidate.Key.Equals(key))
|
|
{
|
|
if (lastIndex != -1)
|
|
{
|
|
entries[lastIndex].Next = candidate.Next;
|
|
}
|
|
else
|
|
{
|
|
this.buckets[bucketIndex] = candidate.Next + 1;
|
|
}
|
|
|
|
entries[entryIndex] = default;
|
|
entries[entryIndex].Next = -3 - this.freeList;
|
|
|
|
this.freeList = entryIndex;
|
|
this.count--;
|
|
|
|
return true;
|
|
}
|
|
|
|
lastIndex = entryIndex;
|
|
entryIndex = candidate.Next;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the value for the specified key, or, if the key is not present,
|
|
/// adds an entry and returns the value by ref. This makes it possible to
|
|
/// add or update a value in a single look up operation.
|
|
/// </summary>
|
|
/// <param name="key">Key to look for</param>
|
|
/// <returns>Reference to the new or existing value</returns>
|
|
public ref TValue? GetOrAddValueRef(TKey key)
|
|
{
|
|
Entry[] entries = this.entries;
|
|
int bucketIndex = key.GetHashCode() & (this.buckets.Length - 1);
|
|
|
|
for (int i = this.buckets[bucketIndex] - 1;
|
|
(uint)i < (uint)entries.Length;
|
|
i = entries[i].Next)
|
|
{
|
|
if (key.Equals(entries[i].Key))
|
|
{
|
|
return ref entries[i].Value;
|
|
}
|
|
}
|
|
|
|
return ref AddKey(key, bucketIndex);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a slot for a new value to add for a specified key.
|
|
/// </summary>
|
|
/// <param name="key">The key to use to add the new value.</param>
|
|
/// <param name="bucketIndex">The target bucked index to use.</param>
|
|
/// <returns>A reference to the slot for the new value to add.</returns>
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
private ref TValue? AddKey(TKey key, int bucketIndex)
|
|
{
|
|
Entry[] entries = this.entries;
|
|
int entryIndex;
|
|
|
|
if (this.freeList != -1)
|
|
{
|
|
entryIndex = this.freeList;
|
|
|
|
this.freeList = -3 - entries[this.freeList].Next;
|
|
}
|
|
else
|
|
{
|
|
if (this.count == entries.Length || entries.Length == 1)
|
|
{
|
|
entries = Resize();
|
|
bucketIndex = key.GetHashCode() & (this.buckets.Length - 1);
|
|
}
|
|
|
|
entryIndex = this.count;
|
|
}
|
|
|
|
entries[entryIndex].Key = key;
|
|
entries[entryIndex].Next = this.buckets[bucketIndex] - 1;
|
|
|
|
this.buckets[bucketIndex] = entryIndex + 1;
|
|
this.count++;
|
|
|
|
return ref entries[entryIndex].Value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resizes the current dictionary to reduce the number of collisions
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
private Entry[] Resize()
|
|
{
|
|
int count = this.count;
|
|
int newSize = this.entries.Length * 2;
|
|
|
|
if ((uint)newSize > int.MaxValue)
|
|
{
|
|
ThrowInvalidOperationExceptionForMaxCapacityExceeded();
|
|
}
|
|
|
|
DictionarySlim<TKey, TValue>.Entry[]? entries = new Entry[newSize];
|
|
|
|
Array.Copy(this.entries, 0, entries, 0, count);
|
|
|
|
int[]? newBuckets = new int[entries.Length];
|
|
|
|
while (count-- > 0)
|
|
{
|
|
int bucketIndex = entries[count].Key.GetHashCode() & (newBuckets.Length - 1);
|
|
|
|
entries[count].Next = newBuckets[bucketIndex] - 1;
|
|
|
|
newBuckets[bucketIndex] = count + 1;
|
|
}
|
|
|
|
this.buckets = newBuckets;
|
|
this.entries = entries;
|
|
|
|
return entries;
|
|
}
|
|
|
|
/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
|
|
[Pure]
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public Enumerator GetEnumerator() => new(this);
|
|
|
|
/// <summary>
|
|
/// Enumerator for <see cref="DictionarySlim{TKey,TValue}"/>.
|
|
/// </summary>
|
|
public ref struct Enumerator
|
|
{
|
|
private readonly Entry[] entries;
|
|
private int index;
|
|
private int count;
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
internal Enumerator(DictionarySlim<TKey, TValue> dictionary)
|
|
{
|
|
this.entries = dictionary.entries;
|
|
this.index = 0;
|
|
this.count = dictionary.count;
|
|
}
|
|
|
|
/// <inheritdoc cref="IEnumerator.MoveNext"/>
|
|
public bool MoveNext()
|
|
{
|
|
if (this.count == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
this.count--;
|
|
|
|
Entry[] entries = this.entries;
|
|
|
|
while (entries[this.index].Next < -1)
|
|
{
|
|
this.index++;
|
|
}
|
|
|
|
// We need to preemptively increment the current index so that we still correctly keep track
|
|
// of the current position in the dictionary even if the users doesn't access any of the
|
|
// available properties in the enumerator. As this is a possibility, we can't rely on one of
|
|
// them to increment the index before MoveNext is invoked again. We ditch the standard enumerator
|
|
// API surface here to expose the Key/Value properties directly and minimize the memory copies.
|
|
// For the same reason, we also removed the KeyValuePair<TKey, TValue> field here, and instead
|
|
// rely on the properties lazily accessing the target instances directly from the current entry
|
|
// pointed at by the index property (adjusted backwards to account for the increment here).
|
|
this.index++;
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the current key.
|
|
/// </summary>
|
|
public TKey Key
|
|
{
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
get => this.entries[this.index - 1].Key;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the current value.
|
|
/// </summary>
|
|
public TValue Value
|
|
{
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
get => this.entries[this.index - 1].Value!;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Throws an <see cref="ArgumentException"/> when trying to load an element with a missing key.
|
|
/// </summary>
|
|
private static void ThrowArgumentExceptionForKeyNotFound(TKey key)
|
|
{
|
|
throw new ArgumentException($"The target key {key} was not present in the dictionary");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Throws an <see cref="InvalidOperationException"/> when trying to resize over the maximum capacity.
|
|
/// </summary>
|
|
private static void ThrowInvalidOperationExceptionForMaxCapacityExceeded()
|
|
{
|
|
throw new InvalidOperationException("Max capacity exceeded");
|
|
}
|
|
}
|