* don't use custom ToArray for small frozen collections, up to 50% gain for creation time for collections with <= 4 items
* for these types GetHashCode returns their value casted to int, so when we receive a Dictionary/HashSet where there are key we know that all hash codes are unique and we can avoid some work later
10-15% CPU time gain and 15-20% allocation reduction for FrozenDictionary and FrozenHashSet where TKey is uint, short, ushort, byte, sbyte
* move Length Buckets code to a dedicated helper type to reduce code duplication and decrease code size
* add tests for Frozen Dictionaries with key being uint, short, ushort, byte, sbyte, nint and nuint
* fix discovered bug: IntPtr started implementing IComparable<IntPtr> in .NET 5
public void ICollection_Generic_Contains_DefaultValueOnCollectionNotContainingDefaultValue(int count)
{
ICollection<T> collection = GenericICollectionFactory(count);
- if (DefaultValueAllowed)
+ if (DefaultValueAllowed && default(T) is null) // it's true only for reference types and for Nullable<T>
+ {
Assert.False(collection.Contains(default(T)));
+ }
}
[Theory]
[MemberData(nameof(ValidCollectionSizes))]
public void IDictionary_Generic_ItemGet_MissingDefaultKey_ThrowsKeyNotFoundException(int count)
{
- if (DefaultValueAllowed)
+ if (DefaultValueAllowed && !IsReadOnly)
{
IDictionary<TKey, TValue> dictionary = GenericIDictionaryFactory(count);
TKey missingKey = default(TKey);
IDictionary<TKey, TValue> dictionary = GenericIDictionaryFactory(count);
if (DefaultValueAllowed)
{
- // returns false
- TKey missingKey = default(TKey);
- while (dictionary.ContainsKey(missingKey))
- dictionary.Remove(missingKey);
- Assert.False(dictionary.ContainsKey(missingKey));
+ if (!IsReadOnly)
+ {
+ // returns false
+ TKey missingKey = default(TKey);
+ while (dictionary.ContainsKey(missingKey))
+ dictionary.Remove(missingKey);
+ Assert.False(dictionary.ContainsKey(missingKey));
+ }
}
else
{
TValue outValue;
if (DefaultValueAllowed)
{
- TKey missingKey = default(TKey);
- while (dictionary.ContainsKey(missingKey))
- dictionary.Remove(missingKey);
- Assert.False(dictionary.TryGetValue(missingKey, out outValue));
+ if (!IsReadOnly)
+ {
+ TKey missingKey = default(TKey);
+ while (dictionary.ContainsKey(missingKey))
+ dictionary.Remove(missingKey);
+ Assert.False(dictionary.TryGetValue(missingKey, out outValue));
+ }
}
else
{
<Compile Include="System\Collections\Frozen\Int32\Int32FrozenSet.cs" />
<Compile Include="System\Collections\Frozen\String\Hashing.cs" />
<Compile Include="System\Collections\Frozen\String\KeyAnalyzer.cs" />
+ <Compile Include="System\Collections\Frozen\String\LengthBuckets.cs" />
<Compile Include="System\Collections\Frozen\String\LengthBucketsFrozenDictionary.cs" />
<Compile Include="System\Collections\Frozen\String\LengthBucketsFrozenSet.cs" />
<Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary.cs" />
typeof(T) == typeof(uint) ||
typeof(T) == typeof(long) ||
typeof(T) == typeof(ulong) ||
- typeof(T) == typeof(nint) ||
- typeof(T) == typeof(nuint) ||
typeof(T) == typeof(decimal) ||
typeof(T) == typeof(float) ||
typeof(T) == typeof(double) ||
#endif
#if NET5_0_OR_GREATER
typeof(T) == typeof(Half) ||
+ typeof(T) == typeof(nint) ||
+ typeof(T) == typeof(nuint) ||
#endif
#if NET6_0_OR_GREATER
typeof(T) == typeof(DateOnly) ||
typeof(T) == typeof(UInt128) ||
#endif
typeof(T).IsEnum;
+
+ // for these types GetHashCode returns their value casted to int, so when we receive a Dictionary/HashSet where there are key
+ // we know that all hash codes are unique and we can avoid some work later
+ internal static bool KeysAreHashCodes<T>()
+ => typeof(T) == typeof(int) || typeof(T) == typeof(uint)
+ || typeof(T) == typeof(short) || typeof(T) == typeof(ushort)
+ || typeof(T) == typeof(byte) || typeof(T) == typeof(sbyte)
+ || ((typeof(T) == typeof(nint) || typeof(T) == typeof(nuint)) && IntPtr.Size == 4);
}
}
private protected readonly FrozenHashTable _hashTable;
private protected readonly T[] _items;
- protected ItemsFrozenSet(HashSet<T> source) : base(source.Comparer)
+ protected ItemsFrozenSet(HashSet<T> source, bool keysAreHashCodes = false) : base(source.Comparer)
{
Debug.Assert(source.Count != 0);
hashCodes[i] = entries[i] is T t ? Comparer.GetHashCode(t) : 0;
}
- _hashTable = FrozenHashTable.Create(hashCodes);
+ _hashTable = FrozenHashTable.Create(hashCodes, keysAreHashCodes);
for (int srcIndex = 0; srcIndex < hashCodes.Length; srcIndex++)
{
private protected readonly TKey[] _keys;
private protected readonly TValue[] _values;
- protected KeysAndValuesFrozenDictionary(Dictionary<TKey, TValue> source) : base(source.Comparer)
+ protected KeysAndValuesFrozenDictionary(Dictionary<TKey, TValue> source, bool keysAreHashCodes = false) : base(source.Comparer)
{
Debug.Assert(source.Count != 0);
hashCodes[i] = Comparer.GetHashCode(entries[i].Key);
}
- _hashTable = FrozenHashTable.Create(hashCodes);
+ _hashTable = FrozenHashTable.Create(hashCodes, keysAreHashCodes);
for (int srcIndex = 0; srcIndex < hashCodes.Length; srcIndex++)
{
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
+using System.Linq;
using System.Runtime.CompilerServices;
namespace System.Collections.Frozen
{
Debug.Assert(source.Count != 0);
- _keys = source.Keys.ToArray(source.Count);
- _values = source.Values.ToArray(source.Count);
+ _keys = source.Keys.ToArray();
+ _values = source.Values.ToArray();
}
private protected override TKey[] KeysCore => _keys;
internal SmallFrozenSet(HashSet<T> source) : base(source.Comparer)
{
- _items = source.ToArray(source.Count);
+ _items = source.ToArray();
}
private protected override T[] ItemsCore => _items;
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Buffers;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace System.Collections.Frozen
+{
+ internal static class LengthBuckets
+ {
+ /// <summary>The maximum number of items allowed per bucket. The larger the value, the longer it can take to search a bucket, which is sequentially examined.</summary>
+ internal const int MaxPerLength = 5;
+ /// <summary>Allowed ratio between buckets with values and total buckets. Under this ratio, this implementation won't be used due to too much wasted space.</summary>
+ private const double EmptyLengthsRatio = 0.2;
+
+ internal static int[]? CreateLengthBucketsArrayIfAppropriate(string[] keys, IEqualityComparer<string> comparer, int minLength, int maxLength)
+ {
+ Debug.Assert(comparer == EqualityComparer<string>.Default || comparer == StringComparer.Ordinal || comparer == StringComparer.OrdinalIgnoreCase);
+ Debug.Assert(minLength >= 0 && maxLength >= minLength);
+
+ // If without even looking at the keys we know that some bucket will exceed the max per-bucket
+ // limit (pigeon hole principle), we can early-exit out without doing any further work.
+ int spread = maxLength - minLength + 1;
+ if (keys.Length / spread > MaxPerLength)
+ {
+ return null;
+ }
+
+ int arraySize = spread * MaxPerLength;
+#if NET6_0_OR_GREATER
+ if (arraySize > Array.MaxLength)
+#else
+ if (arraySize > 0X7FFFFFC7)
+#endif
+ {
+ // In the future we may lower the value, as it may be quite unlikely
+ // to have a LOT of strings of different sizes.
+ return null;
+ }
+
+ // Instead of creating a dictionary of lists or a multi-dimensional array
+ // we rent a single dimension array, where every bucket has five slots.
+ // The bucket starts at (key.Length - minLength) * 5.
+ // Each value is an index of the key from _keys array
+ // or just -1, which represents "null".
+ int[] buckets = ArrayPool<int>.Shared.Rent(arraySize);
+ buckets.AsSpan(0, arraySize).Fill(-1);
+
+ int nonEmptyCount = 0;
+ for (int i = 0; i < keys.Length; i++)
+ {
+ string key = keys[i];
+ int startIndex = (key.Length - minLength) * MaxPerLength;
+ int endIndex = startIndex + MaxPerLength;
+ int index = startIndex;
+
+ while (index < endIndex)
+ {
+ ref int bucket = ref buckets[index];
+ if (bucket < 0)
+ {
+ if (index == startIndex)
+ {
+ nonEmptyCount++;
+ }
+
+ bucket = i;
+ break;
+ }
+
+ index++;
+ }
+
+ if (index == endIndex)
+ {
+ // If we've already hit the max per-bucket limit, bail.
+ ArrayPool<int>.Shared.Return(buckets);
+ return null;
+ }
+ }
+
+ // If there would be too much empty space in the lookup array, bail.
+ if (nonEmptyCount / (double)spread < EmptyLengthsRatio)
+ {
+ ArrayPool<int>.Shared.Return(buckets);
+ return null;
+ }
+
+#if NET6_0_OR_GREATER
+ // We don't need an array with every value initialized to zero if we are just about to overwrite every value anyway.
+ int[] copy = GC.AllocateUninitializedArray<int>(arraySize);
+ Array.Copy(buckets, copy, arraySize);
+#else
+ int[] copy = buckets.AsSpan(0, arraySize).ToArray();
+#endif
+ ArrayPool<int>.Shared.Return(buckets);
+
+ return copy;
+ }
+ }
+}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
/// <summary>Provides a frozen dictionary implementation where strings are grouped by their lengths.</summary>
internal sealed class LengthBucketsFrozenDictionary<TValue> : FrozenDictionary<string, TValue>
{
- /// <summary>Allowed ratio between buckets with values and total buckets. Under this ratio, this implementation won't be used due to too much wasted space.</summary>
- private const double EmptyLengthsRatio = 0.2;
- /// <summary>The maximum number of items allowed per bucket. The larger the value, the longer it can take to search a bucket, which is sequentially examined.</summary>
- private const int MaxPerLength = 5;
-
private readonly int[] _lengthBuckets;
private readonly int _minLength;
private readonly string[] _keys;
string[] keys, TValue[] values, IEqualityComparer<string> comparer, int minLength, int maxLength)
{
Debug.Assert(keys.Length != 0 && keys.Length == values.Length);
- Debug.Assert(comparer == EqualityComparer<string>.Default || comparer == StringComparer.Ordinal || comparer == StringComparer.OrdinalIgnoreCase);
- Debug.Assert(minLength >= 0 && maxLength >= minLength);
- // If without even looking at the keys we know that some bucket will exceed the max per-bucket
- // limit (pigeon hole principle), we can early-exit out without doing any further work.
- int spread = maxLength - minLength + 1;
- if (keys.Length / spread > MaxPerLength)
+ int[]? lengthBuckets = LengthBuckets.CreateLengthBucketsArrayIfAppropriate(keys, comparer, minLength, maxLength);
+ if (lengthBuckets is null)
{
return null;
}
- int arraySize = spread * MaxPerLength;
-#if NET6_0_OR_GREATER
- if (arraySize > Array.MaxLength)
-#else
- if (arraySize > 0X7FFFFFC7)
-#endif
- {
- // In the future we may lower the value, as it may be quite unlikely
- // to have a LOT of strings of different sizes.
- return null;
- }
-
- // Instead of creating a dictionary of lists or a multi-dimensional array
- // we rent a single dimension array, where every bucket has five slots.
- // The bucket starts at (key.Length - minLength) * 5.
- // Each value is an index of the key from _keys array
- // or just -1, which represents "null".
- int[] buckets = ArrayPool<int>.Shared.Rent(arraySize);
- buckets.AsSpan(0, arraySize).Fill(-1);
-
- int nonEmptyCount = 0;
- for (int i = 0; i < keys.Length; i++)
- {
- string key = keys[i];
- int startIndex = (key.Length - minLength) * MaxPerLength;
- int endIndex = startIndex + MaxPerLength;
- int index = startIndex;
-
- while (index < endIndex)
- {
- ref int bucket = ref buckets[index];
- if (bucket < 0)
- {
- if (index == startIndex)
- {
- nonEmptyCount++;
- }
-
- bucket = i;
- break;
- }
-
- index++;
- }
-
- if (index == endIndex)
- {
- // If we've already hit the max per-bucket limit, bail.
- ArrayPool<int>.Shared.Return(buckets);
- return null;
- }
- }
-
- // If there would be too much empty space in the lookup array, bail.
- if (nonEmptyCount / (double)spread < EmptyLengthsRatio)
- {
- ArrayPool<int>.Shared.Return(buckets);
- return null;
- }
-
-#if NET6_0_OR_GREATER
- // We don't need an array with every value initialized to zero if we are just about to overwrite every value anyway.
- int[] copy = GC.AllocateUninitializedArray<int>(arraySize);
- Array.Copy(buckets, copy, arraySize);
-#else
- int[] copy = buckets.AsSpan(0, arraySize).ToArray();
-#endif
- ArrayPool<int>.Shared.Return(buckets);
-
- return new LengthBucketsFrozenDictionary<TValue>(keys, values, copy, minLength, comparer);
+ return new LengthBucketsFrozenDictionary<TValue>(keys, values, lengthBuckets, minLength, comparer);
}
/// <inheritdoc />
private protected override ref readonly TValue GetValueRefOrNullRefCore(string key)
{
// If the length doesn't have an associated bucket, the key isn't in the dictionary.
- int bucketIndex = (key.Length - _minLength) * MaxPerLength;
- int bucketEndIndex = bucketIndex + MaxPerLength;
+ int bucketIndex = (key.Length - _minLength) * LengthBuckets.MaxPerLength;
+ int bucketEndIndex = bucketIndex + LengthBuckets.MaxPerLength;
int[] lengthBuckets = _lengthBuckets;
if (bucketIndex >= 0 && bucketEndIndex <= lengthBuckets.Length)
{
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
/// <summary>Provides a frozen set implementation where strings are grouped by their lengths.</summary>
internal sealed class LengthBucketsFrozenSet : FrozenSetInternalBase<string, LengthBucketsFrozenSet.GSW>
{
- /// <summary>Allowed ratio between buckets with values and total buckets. Under this ratio, this implementation won't be used due to too much wasted space.</summary>
- private const double EmptyLengthsRatio = 0.2;
- /// <summary>The maximum number of items allowed per bucket. The larger the value, the longer it can take to search a bucket, which is sequentially examined.</summary>
- private const int MaxPerLength = 5;
-
private readonly int[] _lengthBuckets;
private readonly int _minLength;
private readonly string[] _items;
string[] items, IEqualityComparer<string> comparer, int minLength, int maxLength)
{
Debug.Assert(items.Length != 0);
- Debug.Assert(comparer == EqualityComparer<string>.Default || comparer == StringComparer.Ordinal || comparer == StringComparer.OrdinalIgnoreCase);
- Debug.Assert(minLength >= 0 && maxLength >= minLength);
- // If without even looking at the keys we know that some bucket will exceed the max per-bucket
- // limit (pigeon hole principle), we can early-exit out without doing any further work.
- int spread = maxLength - minLength + 1;
- if (items.Length / spread > MaxPerLength)
+ int[]? lengthBuckets = LengthBuckets.CreateLengthBucketsArrayIfAppropriate(items, comparer, minLength, maxLength);
+ if (lengthBuckets is null)
{
return null;
}
- int arraySize = spread * MaxPerLength;
-#if NET6_0_OR_GREATER
- if (arraySize > Array.MaxLength)
-#else
- if (arraySize > 0X7FFFFFC7)
-#endif
- {
- // In the future we may lower the value, as it may be quite unlikely
- // to have a LOT of strings of different sizes.
- return null;
- }
-
- // Instead of creating a dictionary of lists or a multi-dimensional array
- // we rent a single dimension array, where every bucket has five slots.
- // The bucket starts at (key.Length - minLength) * 5.
- // Each value is an index of the key from _keys array
- // or just -1, which represents "null".
- int[] buckets = ArrayPool<int>.Shared.Rent(arraySize);
- buckets.AsSpan(0, arraySize).Fill(-1);
-
- int nonEmptyCount = 0;
- for (int i = 0; i < items.Length; i++)
- {
- string key = items[i];
- int startIndex = (key.Length - minLength) * MaxPerLength;
- int endIndex = startIndex + MaxPerLength;
- int index = startIndex;
-
- while (index < endIndex)
- {
- ref int bucket = ref buckets[index];
- if (bucket < 0)
- {
- if (index == startIndex)
- {
- nonEmptyCount++;
- }
-
- bucket = i;
- break;
- }
-
- index++;
- }
-
- if (index == endIndex)
- {
- // If we've already hit the max per-bucket limit, bail.
- ArrayPool<int>.Shared.Return(buckets);
- return null;
- }
- }
-
- // If there would be too much empty space in the lookup array, bail.
- if (nonEmptyCount / (double)spread < EmptyLengthsRatio)
- {
- ArrayPool<int>.Shared.Return(buckets);
- return null;
- }
-
-#if NET6_0_OR_GREATER
- // We don't need an array with every value initialized to zero if we are just about to overwrite every value anyway.
- int[] copy = GC.AllocateUninitializedArray<int>(arraySize);
- Array.Copy(buckets, copy, arraySize);
-#else
- int[] copy = buckets.AsSpan(0, arraySize).ToArray();
-#endif
- ArrayPool<int>.Shared.Return(buckets);
-
- return new LengthBucketsFrozenSet(items, copy, minLength, comparer);
+ return new LengthBucketsFrozenSet(items, lengthBuckets, minLength, comparer);
}
/// <inheritdoc />
if (item is not null) // this implementation won't be constructed from null values, but Contains may still be called with one
{
// If the length doesn't have an associated bucket, the key isn't in the dictionary.
- int bucketIndex = (item.Length - _minLength) * MaxPerLength;
- int bucketEndIndex = bucketIndex + MaxPerLength;
+ int bucketIndex = (item.Length - _minLength) * LengthBuckets.MaxPerLength;
+ int bucketEndIndex = bucketIndex + LengthBuckets.MaxPerLength;
int[] lengthBuckets = _lengthBuckets;
if (bucketIndex >= 0 && bucketEndIndex <= lengthBuckets.Length)
{
}
else
{
- // -1 is used to indicate a null, when it's casted to uint it becomes > keys.Length
+ // -1 is used to indicate a null, when it's casted to uint it becomes > items.Length
break;
}
}
}
else
{
- // -1 is used to indicate a null, when it's casted to unit it becomes > keys.Length
+ // -1 is used to indicate a null, when it's casted to unit it becomes > items.Length
break;
}
}
internal sealed class ValueTypeDefaultComparerFrozenDictionary<TKey, TValue> : KeysAndValuesFrozenDictionary<TKey, TValue>, IDictionary<TKey, TValue>
where TKey : notnull
{
- internal ValueTypeDefaultComparerFrozenDictionary(Dictionary<TKey, TValue> source) : base(source)
+ internal ValueTypeDefaultComparerFrozenDictionary(Dictionary<TKey, TValue> source) : base(source, Constants.KeysAreHashCodes<TKey>())
{
Debug.Assert(typeof(TKey).IsValueType);
Debug.Assert(ReferenceEquals(source.Comparer, EqualityComparer<TKey>.Default));
/// <typeparam name="T">The type of values in the set.</typeparam>
internal sealed class ValueTypeDefaultComparerFrozenSet<T> : ItemsFrozenSet<T, ValueTypeDefaultComparerFrozenSet<T>.GSW>
{
- internal ValueTypeDefaultComparerFrozenSet(HashSet<T> source) : base(source)
+ internal ValueTypeDefaultComparerFrozenSet(HashSet<T> source) : base(source, Constants.KeysAreHashCodes<T>())
{
Debug.Assert(typeof(T).IsValueType);
Debug.Assert(ReferenceEquals(source.Comparer, EqualityComparer<T>.Default));
protected override Type ICollection_Generic_CopyTo_IndexLargerThanArrayCount_ThrowType => typeof(ArgumentOutOfRangeException);
protected virtual bool AllowVeryLargeSizes => true;
+ protected virtual int MaxUniqueValueCount => int.MaxValue;
public virtual TKey GetEqualKey(TKey key) => key;
protected override IDictionary<TKey, TValue> GenericIDictionaryFactory(int count)
+ => GenerateUniqueKeyValuePairs(count).ToFrozenDictionary(GetKeyIEqualityComparer());
+
+ protected KeyValuePair<TKey, TValue>[] GenerateUniqueKeyValuePairs(int count)
{
- var d = new Dictionary<TKey, TValue>();
- for (int i = 0; i < count; i++)
+ if (count > MaxUniqueValueCount)
{
- d.Add(CreateTKey(i), CreateTValue(i));
+ throw new NotSupportedException($"It's impossible to create {count} unique values for {typeof(TKey)} keys.");
+ }
+
+ Dictionary<TKey, TValue> dictionary = new();
+ int seed = 0;
+ while (dictionary.Count != count)
+ {
+ TKey key = CreateTKey(seed);
+ if (!dictionary.ContainsKey(key))
+ {
+ dictionary.Add(key, CreateTValue(seed));
+ }
+ seed++;
}
- return d.ToFrozenDictionary(GetKeyIEqualityComparer());
+ return dictionary.ToArray();
}
protected override IDictionary<TKey, TValue> GenericIDictionaryFactory() => Enumerable.Empty<KeyValuePair<TKey, TValue>>().ToFrozenDictionary();
[Fact]
public void ToFrozenDictionary_KeySelector_ResultsAreUsed()
{
- TKey[] keys = Enumerable.Range(0, 10).Select(CreateTKey).ToArray();
+ TKey[] keys = GenerateUniqueKeyValuePairs(10).Select(pair => pair.Key).ToArray();
FrozenDictionary<TKey, int> frozen = Enumerable.Range(0, 10).ToFrozenDictionary(i => keys[i], NonDefaultEqualityComparer<TKey>.Instance);
Assert.Same(NonDefaultEqualityComparer<TKey>.Instance, frozen.Comparer);
[Fact]
public void ToFrozenDictionary_KeySelectorAndValueSelector_ResultsAreUsed()
{
- TKey[] keys = Enumerable.Range(0, 10).Select(CreateTKey).ToArray();
- TValue[] values = Enumerable.Range(0, 10).Select(CreateTValue).ToArray();
+ KeyValuePair<TKey, TValue>[] uniquePairs = GenerateUniqueKeyValuePairs(10);
+ TKey[] keys = uniquePairs.Select(pair => pair.Key).ToArray();
+ TValue[] values = uniquePairs.Select(pair => pair.Value).ToArray();
FrozenDictionary<TKey, TValue> frozen = Enumerable.Range(0, 10).ToFrozenDictionary(i => keys[i], i => values[i], NonDefaultEqualityComparer<TKey>.Instance);
Assert.Same(NonDefaultEqualityComparer<TKey>.Instance, frozen.Comparer);
public void LookupItems_AllItemsFoundAsExpected(int size, IEqualityComparer<TKey> comparer, bool specifySameComparer)
{
Dictionary<TKey, TValue> original =
- Enumerable.Range(0, size)
- .Select(i => new KeyValuePair<TKey, TValue>(CreateTKey(i), CreateTValue(i)))
+ GenerateUniqueKeyValuePairs(size)
.ToDictionary(p => p.Key, p => p.Value, comparer);
KeyValuePair<TKey, TValue>[] originalPairs = original.ToArray();
public void EqualButPossiblyDifferentKeys_Found(bool fromDictionary)
{
Dictionary<TKey, TValue> original =
- Enumerable.Range(0, 50)
- .Select(i => new KeyValuePair<TKey, TValue>(CreateTKey(i), CreateTValue(i)))
+ GenerateUniqueKeyValuePairs(50)
.ToDictionary(p => p.Key, p => p.Value, GetKeyIEqualityComparer());
FrozenDictionary<TKey, TValue> frozen = fromDictionary ?
[Fact]
public void MultipleValuesSameKey_LastInSourceWins()
{
- TKey[] keys = Enumerable.Range(0, 2).Select(CreateTKey).ToArray();
+ TKey[] keys = GenerateUniqueKeyValuePairs(2).Select(pair => pair.Key).ToArray();
TValue[] values = Enumerable.Range(0, 10).Select(CreateTValue).ToArray();
foreach (bool reverse in new[] { false, true })
}
}
- public class FrozenDictionary_Generic_Tests_int_int : FrozenDictionary_Generic_Tests<int, int>
+ public abstract class FrozenDictionary_Generic_Tests_base_for_numbers<T> : FrozenDictionary_Generic_Tests<T, T>
{
protected override bool DefaultValueAllowed => true;
- protected override KeyValuePair<int, int> CreateT(int seed)
+ protected override KeyValuePair<T, T> CreateT(int seed)
{
Random rand = new Random(seed);
- return new KeyValuePair<int, int>(rand.Next(), rand.Next());
+ return new KeyValuePair<T, T>(Next(rand), Next(rand));
}
- protected override int CreateTKey(int seed) => new Random(seed).Next();
+ protected override T CreateTKey(int seed) => Next(new Random(seed));
+
+ protected override T CreateTValue(int seed) => CreateTKey(seed);
+
+ protected abstract T Next(Random random);
+
+ protected static long NextLong(Random random)
+ {
+ byte[] bytes = new byte[8];
+ random.NextBytes(bytes);
+ return BitConverter.ToInt64(bytes, 0);
+ }
+ }
+
+
+ public class FrozenDictionary_Generic_Tests_int_int : FrozenDictionary_Generic_Tests_base_for_numbers<int>
+ {
+ protected override int Next(Random random) => random.Next();
+ }
+
+ public class FrozenDictionary_Generic_Tests_uint_uint : FrozenDictionary_Generic_Tests_base_for_numbers<uint>
+ {
+ protected override uint Next(Random random) => (uint)random.Next(int.MinValue, int.MaxValue);
+ }
+
+ public class FrozenDictionary_Generic_Tests_nint_nint : FrozenDictionary_Generic_Tests_base_for_numbers<nint>
+ {
+ protected override nint Next(Random random) => IntPtr.Size == sizeof(int)
+ ? random.Next(int.MinValue, int.MaxValue)
+ : (nint)NextLong(random);
+ }
+
+ public class FrozenDictionary_Generic_Tests_nuint_nuint : FrozenDictionary_Generic_Tests_base_for_numbers<nuint>
+ {
+ protected override nuint Next(Random random) => IntPtr.Size == sizeof(int)
+ ? (nuint)random.Next(int.MinValue, int.MaxValue)
+ : (nuint)NextLong(random);
+ }
+
+ public class FrozenDictionary_Generic_Tests_short_short : FrozenDictionary_Generic_Tests_base_for_numbers<short>
+ {
+ protected override bool AllowVeryLargeSizes => false;
+
+ protected override int MaxUniqueValueCount => short.MaxValue - short.MinValue;
+
+ protected override short Next(Random random) => (short)random.Next(short.MinValue, short.MaxValue);
+ }
+
+ public class FrozenDictionary_Generic_Tests_ushort_ushort : FrozenDictionary_Generic_Tests_base_for_numbers<ushort>
+ {
+ protected override bool AllowVeryLargeSizes => false;
+
+ protected override int MaxUniqueValueCount => ushort.MaxValue;
+
+ protected override ushort Next(Random random) => (ushort)random.Next(ushort.MinValue, ushort.MaxValue);
+ }
+
+ public class FrozenDictionary_Generic_Tests_byte_byte : FrozenDictionary_Generic_Tests_base_for_numbers<byte>
+ {
+ protected override bool AllowVeryLargeSizes => false;
+
+ protected override int MaxUniqueValueCount => byte.MaxValue;
+
+ protected override byte Next(Random random) => (byte)random.Next(byte.MinValue, byte.MaxValue);
+ }
+
+ public class FrozenDictionary_Generic_Tests_sbyte_sbyte : FrozenDictionary_Generic_Tests_base_for_numbers<sbyte>
+ {
+ protected override bool AllowVeryLargeSizes => false;
+
+ protected override int MaxUniqueValueCount => sbyte.MaxValue - sbyte.MinValue;
- protected override int CreateTValue(int seed) => CreateTKey(seed);
+ protected override sbyte Next(Random random) => (sbyte)random.Next(sbyte.MinValue, sbyte.MaxValue);
}
public class FrozenDictionary_Generic_Tests_SimpleClass_SimpleClass : FrozenDictionary_Generic_Tests<SimpleClass, SimpleClass>