diff --git a/src/libraries/Common/tests/System/Collections/ICollection.Generic.Tests.cs b/src/libraries/Common/tests/System/Collections/ICollection.Generic.Tests.cs index d01bb5ca0aa9b..a94a9308b99ab 100644 --- a/src/libraries/Common/tests/System/Collections/ICollection.Generic.Tests.cs +++ b/src/libraries/Common/tests/System/Collections/ICollection.Generic.Tests.cs @@ -406,8 +406,10 @@ public void ICollection_Generic_Contains_ValidValueOnCollectionContainingThatVal public void ICollection_Generic_Contains_DefaultValueOnCollectionNotContainingDefaultValue(int count) { ICollection collection = GenericICollectionFactory(count); - if (DefaultValueAllowed) + if (DefaultValueAllowed && default(T) is null) // it's true only for reference types and for Nullable + { Assert.False(collection.Contains(default(T))); + } } [Theory] diff --git a/src/libraries/Common/tests/System/Collections/IDictionary.Generic.Tests.cs b/src/libraries/Common/tests/System/Collections/IDictionary.Generic.Tests.cs index 22f3912b65e7d..df92ab206f668 100644 --- a/src/libraries/Common/tests/System/Collections/IDictionary.Generic.Tests.cs +++ b/src/libraries/Common/tests/System/Collections/IDictionary.Generic.Tests.cs @@ -289,7 +289,7 @@ public void IDictionary_Generic_ItemGet_MissingNonDefaultKey_ThrowsKeyNotFoundEx [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_Generic_ItemGet_MissingDefaultKey_ThrowsKeyNotFoundException(int count) { - if (DefaultValueAllowed) + if (DefaultValueAllowed && !IsReadOnly) { IDictionary dictionary = GenericIDictionaryFactory(count); TKey missingKey = default(TKey); @@ -733,11 +733,14 @@ public void IDictionary_Generic_ContainsKey_DefaultKeyNotContainedInDictionary(i IDictionary 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 { @@ -934,10 +937,13 @@ public void IDictionary_Generic_TryGetValue_DefaultKeyNotContainedInDictionary(i 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 { diff --git a/src/libraries/System.Collections.Immutable/src/System.Collections.Immutable.csproj b/src/libraries/System.Collections.Immutable/src/System.Collections.Immutable.csproj index 0e0d45309926d..62da301516b2a 100644 --- a/src/libraries/System.Collections.Immutable/src/System.Collections.Immutable.csproj +++ b/src/libraries/System.Collections.Immutable/src/System.Collections.Immutable.csproj @@ -38,6 +38,7 @@ The System.Collections.Immutable library is built-in as part of the shared frame + diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Constants.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Constants.cs index 55e654a1e3029..5371a0354aeac 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Constants.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Constants.cs @@ -53,8 +53,6 @@ public static bool IsKnownComparable() => 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) || @@ -68,6 +66,8 @@ public static bool IsKnownComparable() => #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) || @@ -78,5 +78,13 @@ public static bool IsKnownComparable() => 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() + => 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); } } diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ItemsFrozenSet.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ItemsFrozenSet.cs index 787899b3862ae..f94cbd49e2476 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ItemsFrozenSet.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ItemsFrozenSet.cs @@ -14,7 +14,7 @@ internal abstract class ItemsFrozenSet : FrozenSetInternalBase< private protected readonly FrozenHashTable _hashTable; private protected readonly T[] _items; - protected ItemsFrozenSet(HashSet source) : base(source.Comparer) + protected ItemsFrozenSet(HashSet source, bool keysAreHashCodes = false) : base(source.Comparer) { Debug.Assert(source.Count != 0); @@ -30,7 +30,7 @@ protected ItemsFrozenSet(HashSet source) : base(source.Comparer) 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++) { diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/KeysAndValuesFrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/KeysAndValuesFrozenDictionary.cs index 6435eb1ab3e27..5e76b5888090f 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/KeysAndValuesFrozenDictionary.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/KeysAndValuesFrozenDictionary.cs @@ -15,7 +15,7 @@ internal abstract class KeysAndValuesFrozenDictionary : FrozenDict private protected readonly TKey[] _keys; private protected readonly TValue[] _values; - protected KeysAndValuesFrozenDictionary(Dictionary source) : base(source.Comparer) + protected KeysAndValuesFrozenDictionary(Dictionary source, bool keysAreHashCodes = false) : base(source.Comparer) { Debug.Assert(source.Count != 0); @@ -32,7 +32,7 @@ protected KeysAndValuesFrozenDictionary(Dictionary source) : base( hashCodes[i] = Comparer.GetHashCode(entries[i].Key); } - _hashTable = FrozenHashTable.Create(hashCodes); + _hashTable = FrozenHashTable.Create(hashCodes, keysAreHashCodes); for (int srcIndex = 0; srcIndex < hashCodes.Length; srcIndex++) { diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/SmallFrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/SmallFrozenDictionary.cs index 733e028109a97..317b7c238403c 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/SmallFrozenDictionary.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/SmallFrozenDictionary.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.Linq; using System.Runtime.CompilerServices; namespace System.Collections.Frozen @@ -24,8 +25,8 @@ internal SmallFrozenDictionary(Dictionary source) : base(source.Co { 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; diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/SmallFrozenSet.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/SmallFrozenSet.cs index d5a2c86bd484d..53b89d717686e 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/SmallFrozenSet.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/SmallFrozenSet.cs @@ -18,7 +18,7 @@ internal sealed class SmallFrozenSet : FrozenSetInternalBase source) : base(source.Comparer) { - _items = source.ToArray(source.Count); + _items = source.ToArray(); } private protected override T[] ItemsCore => _items; diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/LengthBuckets.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/LengthBuckets.cs new file mode 100644 index 0000000000000..97659c5479365 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/LengthBuckets.cs @@ -0,0 +1,102 @@ +// 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 + { + /// 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. + internal const int MaxPerLength = 5; + /// Allowed ratio between buckets with values and total buckets. Under this ratio, this implementation won't be used due to too much wasted space. + private const double EmptyLengthsRatio = 0.2; + + internal static int[]? CreateLengthBucketsArrayIfAppropriate(string[] keys, IEqualityComparer comparer, int minLength, int maxLength) + { + Debug.Assert(comparer == EqualityComparer.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.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.Shared.Return(buckets); + return null; + } + } + + // If there would be too much empty space in the lookup array, bail. + if (nonEmptyCount / (double)spread < EmptyLengthsRatio) + { + ArrayPool.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(arraySize); + Array.Copy(buckets, copy, arraySize); +#else + int[] copy = buckets.AsSpan(0, arraySize).ToArray(); +#endif + ArrayPool.Shared.Return(buckets); + + return copy; + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/LengthBucketsFrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/LengthBucketsFrozenDictionary.cs index 3e335bdaa679c..6942face1925f 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/LengthBucketsFrozenDictionary.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/LengthBucketsFrozenDictionary.cs @@ -1,7 +1,6 @@ // 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; @@ -11,11 +10,6 @@ namespace System.Collections.Frozen /// Provides a frozen dictionary implementation where strings are grouped by their lengths. internal sealed class LengthBucketsFrozenDictionary : FrozenDictionary { - /// Allowed ratio between buckets with values and total buckets. Under this ratio, this implementation won't be used due to too much wasted space. - private const double EmptyLengthsRatio = 0.2; - /// 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. - private const int MaxPerLength = 5; - private readonly int[] _lengthBuckets; private readonly int _minLength; private readonly string[] _keys; @@ -39,87 +33,14 @@ private LengthBucketsFrozenDictionary( string[] keys, TValue[] values, IEqualityComparer comparer, int minLength, int maxLength) { Debug.Assert(keys.Length != 0 && keys.Length == values.Length); - Debug.Assert(comparer == EqualityComparer.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.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.Shared.Return(buckets); - return null; - } - } - - // If there would be too much empty space in the lookup array, bail. - if (nonEmptyCount / (double)spread < EmptyLengthsRatio) - { - ArrayPool.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(arraySize); - Array.Copy(buckets, copy, arraySize); -#else - int[] copy = buckets.AsSpan(0, arraySize).ToArray(); -#endif - ArrayPool.Shared.Return(buckets); - - return new LengthBucketsFrozenDictionary(keys, values, copy, minLength, comparer); + return new LengthBucketsFrozenDictionary(keys, values, lengthBuckets, minLength, comparer); } /// @@ -138,8 +59,8 @@ private LengthBucketsFrozenDictionary( 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) { diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/LengthBucketsFrozenSet.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/LengthBucketsFrozenSet.cs index b5e7cd3a4fb98..000eec5f283f1 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/LengthBucketsFrozenSet.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/LengthBucketsFrozenSet.cs @@ -1,7 +1,6 @@ // 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; @@ -10,11 +9,6 @@ namespace System.Collections.Frozen /// Provides a frozen set implementation where strings are grouped by their lengths. internal sealed class LengthBucketsFrozenSet : FrozenSetInternalBase { - /// Allowed ratio between buckets with values and total buckets. Under this ratio, this implementation won't be used due to too much wasted space. - private const double EmptyLengthsRatio = 0.2; - /// 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. - private const int MaxPerLength = 5; - private readonly int[] _lengthBuckets; private readonly int _minLength; private readonly string[] _items; @@ -36,87 +30,14 @@ private LengthBucketsFrozenSet( string[] items, IEqualityComparer comparer, int minLength, int maxLength) { Debug.Assert(items.Length != 0); - Debug.Assert(comparer == EqualityComparer.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.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.Shared.Return(buckets); - return null; - } - } - - // If there would be too much empty space in the lookup array, bail. - if (nonEmptyCount / (double)spread < EmptyLengthsRatio) - { - ArrayPool.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(arraySize); - Array.Copy(buckets, copy, arraySize); -#else - int[] copy = buckets.AsSpan(0, arraySize).ToArray(); -#endif - ArrayPool.Shared.Return(buckets); - - return new LengthBucketsFrozenSet(items, copy, minLength, comparer); + return new LengthBucketsFrozenSet(items, lengthBuckets, minLength, comparer); } /// @@ -134,8 +55,8 @@ private protected override int FindItemIndex(string? item) 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) { @@ -155,7 +76,7 @@ private protected override int FindItemIndex(string? item) } 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; } } @@ -174,7 +95,7 @@ private protected override int FindItemIndex(string? item) } 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; } } diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ValueTypeDefaultComparerFrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ValueTypeDefaultComparerFrozenDictionary.cs index d5630d7580036..661aec613e4f7 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ValueTypeDefaultComparerFrozenDictionary.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ValueTypeDefaultComparerFrozenDictionary.cs @@ -13,7 +13,7 @@ namespace System.Collections.Frozen internal sealed class ValueTypeDefaultComparerFrozenDictionary : KeysAndValuesFrozenDictionary, IDictionary where TKey : notnull { - internal ValueTypeDefaultComparerFrozenDictionary(Dictionary source) : base(source) + internal ValueTypeDefaultComparerFrozenDictionary(Dictionary source) : base(source, Constants.KeysAreHashCodes()) { Debug.Assert(typeof(TKey).IsValueType); Debug.Assert(ReferenceEquals(source.Comparer, EqualityComparer.Default)); diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ValueTypeDefaultComparerFrozenSet.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ValueTypeDefaultComparerFrozenSet.cs index d54dffdc1aa09..8f6322699f904 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ValueTypeDefaultComparerFrozenSet.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ValueTypeDefaultComparerFrozenSet.cs @@ -10,7 +10,7 @@ namespace System.Collections.Frozen /// The type of values in the set. internal sealed class ValueTypeDefaultComparerFrozenSet : ItemsFrozenSet.GSW> { - internal ValueTypeDefaultComparerFrozenSet(HashSet source) : base(source) + internal ValueTypeDefaultComparerFrozenSet(HashSet source) : base(source, Constants.KeysAreHashCodes()) { Debug.Assert(typeof(T).IsValueType); Debug.Assert(ReferenceEquals(source.Comparer, EqualityComparer.Default)); diff --git a/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenDictionaryTests.cs b/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenDictionaryTests.cs index c6455a1609cc8..a1f6adc0c782b 100644 --- a/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenDictionaryTests.cs +++ b/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenDictionaryTests.cs @@ -18,18 +18,33 @@ public abstract class FrozenDictionary_Generic_Tests : IDictionary 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 GenericIDictionaryFactory(int count) + => GenerateUniqueKeyValuePairs(count).ToFrozenDictionary(GetKeyIEqualityComparer()); + + protected KeyValuePair[] GenerateUniqueKeyValuePairs(int count) { - var d = new Dictionary(); - 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 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 GenericIDictionaryFactory() => Enumerable.Empty>().ToFrozenDictionary(); @@ -146,7 +161,7 @@ public void FrozenDictionary_ToFrozenDictionary_Idempotent() [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 frozen = Enumerable.Range(0, 10).ToFrozenDictionary(i => keys[i], NonDefaultEqualityComparer.Instance); Assert.Same(NonDefaultEqualityComparer.Instance, frozen.Comparer); @@ -160,8 +175,9 @@ public void ToFrozenDictionary_KeySelector_ResultsAreUsed() [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[] uniquePairs = GenerateUniqueKeyValuePairs(10); + TKey[] keys = uniquePairs.Select(pair => pair.Key).ToArray(); + TValue[] values = uniquePairs.Select(pair => pair.Value).ToArray(); FrozenDictionary frozen = Enumerable.Range(0, 10).ToFrozenDictionary(i => keys[i], i => values[i], NonDefaultEqualityComparer.Instance); Assert.Same(NonDefaultEqualityComparer.Instance, frozen.Comparer); @@ -183,8 +199,7 @@ public static IEnumerable LookupItems_AllItemsFoundAsExpected_MemberDa public void LookupItems_AllItemsFoundAsExpected(int size, IEqualityComparer comparer, bool specifySameComparer) { Dictionary original = - Enumerable.Range(0, size) - .Select(i => new KeyValuePair(CreateTKey(i), CreateTValue(i))) + GenerateUniqueKeyValuePairs(size) .ToDictionary(p => p.Key, p => p.Value, comparer); KeyValuePair[] originalPairs = original.ToArray(); @@ -237,8 +252,7 @@ comparer is null || public void EqualButPossiblyDifferentKeys_Found(bool fromDictionary) { Dictionary original = - Enumerable.Range(0, 50) - .Select(i => new KeyValuePair(CreateTKey(i), CreateTValue(i))) + GenerateUniqueKeyValuePairs(50) .ToDictionary(p => p.Key, p => p.Value, GetKeyIEqualityComparer()); FrozenDictionary frozen = fromDictionary ? @@ -259,7 +273,7 @@ public void EqualButPossiblyDifferentKeys_Found(bool 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 }) @@ -392,19 +406,89 @@ public void CreateHugeDictionary_Success(int largeCount) } } - public class FrozenDictionary_Generic_Tests_int_int : FrozenDictionary_Generic_Tests + public abstract class FrozenDictionary_Generic_Tests_base_for_numbers : FrozenDictionary_Generic_Tests { protected override bool DefaultValueAllowed => true; - protected override KeyValuePair CreateT(int seed) + protected override KeyValuePair CreateT(int seed) { Random rand = new Random(seed); - return new KeyValuePair(rand.Next(), rand.Next()); + return new KeyValuePair(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 + { + protected override int Next(Random random) => random.Next(); + } + + public class FrozenDictionary_Generic_Tests_uint_uint : FrozenDictionary_Generic_Tests_base_for_numbers + { + 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 + { + 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 + { + 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 + { + 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 + { + 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 + { + 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 + { + 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