From 7a025a60ca79e4fd97d069f48af51003d93d5a0f Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 28 Oct 2022 16:02:36 -0400 Subject: [PATCH] Overhaul frozen collections (API, implementation, tests) --- .../Collections/ICollection.Generic.Tests.cs | 4 +- .../ICollection.NonGeneric.Tests.cs | 5 +- .../Collections/IDictionary.Generic.Tests.cs | 192 +++--- .../IDictionary.NonGeneric.Tests.cs | 216 +++--- .../System/Collections/ISet.Generic.Tests.cs | 158 +++-- .../System.Collections.Immutable/ref/Stubs.cs | 72 -- .../ref/System.Collections.Immutable.cs | 321 ++------- .../ref/System.Collections.Immutable.csproj | 1 - ...System.Collections.Immutable.netcoreapp.cs | 6 + .../src/Resources/Strings.resx | 20 +- .../src/System.Collections.Immutable.csproj | 42 +- .../Frozen/DefaultFrozenDictionary.cs | 44 ++ .../Collections/Frozen/DefaultFrozenSet.cs | 52 ++ .../Frozen/EmptyFrozenDictionary.cs | 31 + .../Collections/Frozen/EmptyFrozenSet.cs | 49 ++ .../src/System/Collections/Frozen/Freezer.cs | 190 ------ .../Collections/Frozen/FrozenDictionary.cs | 643 +++++++++++------- .../Collections/Frozen/FrozenEnumerator.cs | 78 --- .../Collections/Frozen/FrozenHashTable.cs | 233 +++---- .../Collections/Frozen/FrozenIntDictionary.cs | 300 -------- .../System/Collections/Frozen/FrozenIntSet.cs | 218 ------ .../System/Collections/Frozen/FrozenList.cs | 129 ---- .../Frozen/FrozenOrdinalStringDictionary.cs | 347 ---------- .../Frozen/FrozenOrdinalStringSet.cs | 247 ------- .../Frozen/FrozenPairEnumerator.cs | 81 --- .../System/Collections/Frozen/FrozenSet.cs | 492 ++++++++------ .../Frozen/FrozenSetInternalBase.cs | 280 ++++++++ .../System/Collections/Frozen/IFindItem.cs | 19 - .../Collections/Frozen/IFrozenDictionary.cs | 62 -- .../Frozen/IFrozenDictionaryDebugView.cs | 22 - .../Frozen/IFrozenIntDictionaryDebugView.cs | 21 - ...IFrozenOrdinalStringDictionaryDebugView.cs | 21 - .../System/Collections/Frozen/IFrozenSet.cs | 36 - .../Frozen/IReadOnlyCollectionDebugView.cs | 21 - .../Frozen/Int32FrozenDictionary.cs | 68 ++ .../Collections/Frozen/Int32FrozenSet.cs | 66 ++ .../Collections/Frozen/ItemsFrozenSet.cs | 41 ++ .../Frozen/KeysAndValuesFrozenDictionary.cs | 50 ++ .../Frozen/LengthBucketsFrozenDictionary.cs | 150 ++++ .../Frozen/LengthBucketsFrozenSet.cs | 153 +++++ .../Frozen/OrdinalStringFrozenDictionary.cs | 89 +++ .../Frozen/OrdinalStringFrozenSet.cs | 90 +++ .../System/Collections/Frozen/SetSupport.cs | 394 ----------- .../Frozen/StringComparers/ComparerPicker.cs | 63 +- .../FullCaseInsensitiveAsciiStringComparer.cs | 5 +- .../FullCaseInsensitiveStringComparer.cs | 5 +- .../StringComparers/FullStringComparer.cs | 4 +- .../Frozen/StringComparers/Hashing.cs | 110 --- ...edCaseInsensitiveAsciiSubstringComparer.cs | 5 +- ...stifiedCaseInsensitiveSubstringComparer.cs | 5 +- .../LeftJustifiedSingleCharComparer.cs | 2 +- .../LeftJustifiedSubstringComparer.cs | 6 +- ...edCaseInsensitiveAsciiSubstringComparer.cs | 5 +- ...stifiedCaseInsensitiveSubstringComparer.cs | 5 +- .../RightJustifiedSingleCharComparer.cs | 2 +- .../RightJustifiedSubstringComparer.cs | 6 +- .../StringComparers/StringComparerBase.cs | 116 +++- .../StringComparers/SubstringComparerBase.cs | 2 +- ...alueTypeDefaultComparerFrozenDictionary.cs | 44 ++ .../ValueTypeDefaultComparerFrozenSet.cs | 52 ++ .../ImmutableEnumerableDebuggerProxy.cs | 2 +- .../src/System/Collections/ThrowHelper.cs | 26 +- .../src/System/Polyfills.cs | 57 ++ .../src/System/Stubs.cs | 102 --- .../src/Validation/Requires.cs | 9 +- .../Validation/ValidatedNotNullAttribute.cs | 15 - .../tests/Frozen/FrozenDictionaryTest.cs | 480 +++++++++++++ .../tests/Frozen/FrozenSetTests.cs | 274 ++++++++ .../System.Collections.Immutable.Tests.csproj | 61 +- .../src/System/Collections/HashHelpers.cs | 2 +- 70 files changed, 3544 insertions(+), 3675 deletions(-) delete mode 100644 src/libraries/System.Collections.Immutable/ref/Stubs.cs create mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/DefaultFrozenDictionary.cs create mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/DefaultFrozenSet.cs create mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/EmptyFrozenDictionary.cs create mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/EmptyFrozenSet.cs delete mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Freezer.cs delete mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenEnumerator.cs delete mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenIntDictionary.cs delete mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenIntSet.cs delete mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenList.cs delete mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenOrdinalStringDictionary.cs delete mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenOrdinalStringSet.cs delete mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenPairEnumerator.cs create mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenSetInternalBase.cs delete mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IFindItem.cs delete mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IFrozenDictionary.cs delete mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IFrozenDictionaryDebugView.cs delete mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IFrozenIntDictionaryDebugView.cs delete mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IFrozenOrdinalStringDictionaryDebugView.cs delete mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IFrozenSet.cs delete mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IReadOnlyCollectionDebugView.cs create mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Int32FrozenDictionary.cs create mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Int32FrozenSet.cs create mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ItemsFrozenSet.cs create mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/KeysAndValuesFrozenDictionary.cs create mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/LengthBucketsFrozenDictionary.cs create mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/LengthBucketsFrozenSet.cs create mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/OrdinalStringFrozenDictionary.cs create mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/OrdinalStringFrozenSet.cs delete mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/SetSupport.cs delete mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/Hashing.cs create mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ValueTypeDefaultComparerFrozenDictionary.cs create mode 100644 src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ValueTypeDefaultComparerFrozenSet.cs create mode 100644 src/libraries/System.Collections.Immutable/src/System/Polyfills.cs delete mode 100644 src/libraries/System.Collections.Immutable/src/System/Stubs.cs delete mode 100644 src/libraries/System.Collections.Immutable/src/Validation/ValidatedNotNullAttribute.cs create mode 100644 src/libraries/System.Collections.Immutable/tests/Frozen/FrozenDictionaryTest.cs create mode 100644 src/libraries/System.Collections.Immutable/tests/Frozen/FrozenSetTests.cs 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 bc528e67c0a7e..d01bb5ca0aa9b 100644 --- a/src/libraries/Common/tests/System/Collections/ICollection.Generic.Tests.cs +++ b/src/libraries/Common/tests/System/Collections/ICollection.Generic.Tests.cs @@ -489,7 +489,7 @@ public void ICollection_Generic_CopyTo_IndexEqualToArrayCount_ThrowsArgumentExce ICollection collection = GenericICollectionFactory(count); T[] array = new T[count]; if (count > 0) - Assert.Throws(() => collection.CopyTo(array, count)); + Assert.ThrowsAny(() => collection.CopyTo(array, count)); else collection.CopyTo(array, count); // does nothing since the array is empty } @@ -511,7 +511,7 @@ public void ICollection_Generic_CopyTo_NotEnoughSpaceInOffsettedArray_ThrowsArgu { ICollection collection = GenericICollectionFactory(count); T[] array = new T[count]; - Assert.Throws(() => collection.CopyTo(array, 1)); + Assert.ThrowsAny(() => collection.CopyTo(array, 1)); } } diff --git a/src/libraries/Common/tests/System/Collections/ICollection.NonGeneric.Tests.cs b/src/libraries/Common/tests/System/Collections/ICollection.NonGeneric.Tests.cs index 815e4c6b08380..6f73030eb02ac 100644 --- a/src/libraries/Common/tests/System/Collections/ICollection.NonGeneric.Tests.cs +++ b/src/libraries/Common/tests/System/Collections/ICollection.NonGeneric.Tests.cs @@ -154,7 +154,10 @@ public void ICollection_NonGeneric_SyncRootUnique(int count) { ICollection collection1 = NonGenericICollectionFactory(count); ICollection collection2 = NonGenericICollectionFactory(count); - Assert.NotSame(collection1.SyncRoot, collection2.SyncRoot); + if (!ReferenceEquals(collection1, collection2)) + { + Assert.NotSame(collection1.SyncRoot, collection2.SyncRoot); + } } } 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 ce7682dd36f6c..e57af446e8dfc 100644 --- a/src/libraries/Common/tests/System/Collections/IDictionary.Generic.Tests.cs +++ b/src/libraries/Common/tests/System/Collections/IDictionary.Generic.Tests.cs @@ -260,16 +260,19 @@ protected override IEnumerable GetModifyEnumerables(ModifyOper [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_Generic_ItemGet_DefaultKey(int count) { - IDictionary dictionary = GenericIDictionaryFactory(count); - if (!DefaultValueAllowed) - { - Assert.Throws(() => dictionary[default(TKey)]); - } - else + if (!IsReadOnly) { - TValue value = CreateTValue(3452); - dictionary[default(TKey)] = value; - Assert.Equal(value, dictionary[default(TKey)]); + IDictionary dictionary = GenericIDictionaryFactory(count); + if (!DefaultValueAllowed) + { + Assert.Throws(() => dictionary[default(TKey)]); + } + else + { + TValue value = CreateTValue(3452); + dictionary[default(TKey)] = value; + Assert.Equal(value, dictionary[default(TKey)]); + } } } @@ -315,16 +318,19 @@ public void IDictionary_Generic_ItemGet_PresentKeyReturnsCorrectValue(int count) [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_Generic_ItemSet_DefaultKey(int count) { - IDictionary dictionary = GenericIDictionaryFactory(count); - if (!DefaultValueAllowed) - { - Assert.Throws(() => dictionary[default(TKey)] = CreateTValue(3)); - } - else + if (!IsReadOnly) { - TValue value = CreateTValue(3452); - dictionary[default(TKey)] = value; - Assert.Equal(value, dictionary[default(TKey)]); + IDictionary dictionary = GenericIDictionaryFactory(count); + if (!DefaultValueAllowed) + { + Assert.Throws(() => dictionary[default(TKey)] = CreateTValue(3)); + } + else + { + TValue value = CreateTValue(3452); + dictionary[default(TKey)] = value; + Assert.Equal(value, dictionary[default(TKey)]); + } } } @@ -344,23 +350,29 @@ public void IDictionary_Generic_ItemSet_OnReadOnlyDictionary_ThrowsNotSupportedE [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_Generic_ItemSet_AddsNewValueWhenNotPresent(int count) { - IDictionary dictionary = GenericIDictionaryFactory(count); - TKey missingKey = GetNewKey(dictionary); - dictionary[missingKey] = CreateTValue(543); - Assert.Equal(count + 1, dictionary.Count); + if (!IsReadOnly) + { + IDictionary dictionary = GenericIDictionaryFactory(count); + TKey missingKey = GetNewKey(dictionary); + dictionary[missingKey] = CreateTValue(543); + Assert.Equal(count + 1, dictionary.Count); + } } [Theory] [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_Generic_ItemSet_ReplacesExistingValueWhenPresent(int count) { - IDictionary dictionary = GenericIDictionaryFactory(count); - TKey existingKey = GetNewKey(dictionary); - dictionary.Add(existingKey, CreateTValue(5342)); - TValue newValue = CreateTValue(1234); - dictionary[existingKey] = newValue; - Assert.Equal(count + 1, dictionary.Count); - Assert.Equal(newValue, dictionary[existingKey]); + if (!IsReadOnly) + { + IDictionary dictionary = GenericIDictionaryFactory(count); + TKey existingKey = GetNewKey(dictionary); + dictionary.Add(existingKey, CreateTValue(5342)); + TValue newValue = CreateTValue(1234); + dictionary[existingKey] = newValue; + Assert.Equal(count + 1, dictionary.Count); + Assert.Equal(newValue, dictionary[existingKey]); + } } #endregion @@ -380,19 +392,22 @@ public void IDictionary_Generic_Keys_ContainsAllCorrectKeys(int count) [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_Generic_Keys_ModifyingTheDictionaryUpdatesTheCollection(int count) { - IDictionary dictionary = GenericIDictionaryFactory(count); - ICollection keys = dictionary.Keys; - int previousCount = keys.Count; - if (count > 0) - Assert.NotEmpty(keys); - dictionary.Clear(); - if (IDictionary_Generic_Keys_Values_ModifyingTheDictionaryUpdatesTheCollection) - { - Assert.Empty(keys); - } - else + if (!IsReadOnly) { - Assert.Equal(previousCount, keys.Count); + IDictionary dictionary = GenericIDictionaryFactory(count); + ICollection keys = dictionary.Keys; + int previousCount = keys.Count; + if (count > 0) + Assert.NotEmpty(keys); + dictionary.Clear(); + if (IDictionary_Generic_Keys_Values_ModifyingTheDictionaryUpdatesTheCollection) + { + Assert.Empty(keys); + } + else + { + Assert.Equal(previousCount, keys.Count); + } } } @@ -400,22 +415,25 @@ public void IDictionary_Generic_Keys_ModifyingTheDictionaryUpdatesTheCollection( [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_Generic_Keys_Enumeration_ParentDictionaryModifiedInvalidates(int count) { - IDictionary dictionary = GenericIDictionaryFactory(count); - ICollection keys = dictionary.Keys; - IEnumerator keysEnum = keys.GetEnumerator(); - dictionary.Add(GetNewKey(dictionary), CreateTValue(3432)); - if (IDictionary_Generic_Keys_Values_Enumeration_ThrowsInvalidOperation_WhenParentModified) - { - Assert.Throws(() => keysEnum.MoveNext()); - Assert.Throws(() => keysEnum.Reset()); - } - else + if (!IsReadOnly) { - if (keysEnum.MoveNext()) + IDictionary dictionary = GenericIDictionaryFactory(count); + ICollection keys = dictionary.Keys; + IEnumerator keysEnum = keys.GetEnumerator(); + dictionary.Add(GetNewKey(dictionary), CreateTValue(3432)); + if (IDictionary_Generic_Keys_Values_Enumeration_ThrowsInvalidOperation_WhenParentModified) { - _ = keysEnum.Current; + Assert.Throws(() => keysEnum.MoveNext()); + Assert.Throws(() => keysEnum.Reset()); + } + else + { + if (keysEnum.MoveNext()) + { + _ = keysEnum.Current; + } + keysEnum.Reset(); } - keysEnum.Reset(); } } @@ -461,16 +479,19 @@ public void IDictionary_Generic_Values_ContainsAllCorrectValues(int count) [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_Generic_Values_IncludeDuplicatesMultipleTimes(int count) { - IDictionary dictionary = GenericIDictionaryFactory(count); - int seed = 431; - foreach (KeyValuePair pair in dictionary.ToList()) + if (!IsReadOnly) { - TKey missingKey = CreateTKey(seed++); - while (dictionary.ContainsKey(missingKey)) - missingKey = CreateTKey(seed++); - dictionary.Add(missingKey, pair.Value); + IDictionary dictionary = GenericIDictionaryFactory(count); + int seed = 431; + foreach (KeyValuePair pair in dictionary.ToList()) + { + TKey missingKey = CreateTKey(seed++); + while (dictionary.ContainsKey(missingKey)) + missingKey = CreateTKey(seed++); + dictionary.Add(missingKey, pair.Value); + } + Assert.Equal(count * 2, dictionary.Values.Count); } - Assert.Equal(count * 2, dictionary.Values.Count); } [Theory] @@ -482,14 +503,18 @@ public void IDictionary_Generic_Values_ModifyingTheDictionaryUpdatesTheCollectio int previousCount = values.Count; if (count > 0) Assert.NotEmpty(values); - dictionary.Clear(); - if (IDictionary_Generic_Keys_Values_ModifyingTheDictionaryUpdatesTheCollection) - { - Assert.Empty(values); - } - else + + if (!IsReadOnly) { - Assert.Equal(previousCount, values.Count); + dictionary.Clear(); + if (IDictionary_Generic_Keys_Values_ModifyingTheDictionaryUpdatesTheCollection) + { + Assert.Empty(values); + } + else + { + Assert.Equal(previousCount, values.Count); + } } } @@ -497,22 +522,25 @@ public void IDictionary_Generic_Values_ModifyingTheDictionaryUpdatesTheCollectio [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_Generic_Values_Enumeration_ParentDictionaryModifiedInvalidates(int count) { - IDictionary dictionary = GenericIDictionaryFactory(count); - ICollection values = dictionary.Values; - IEnumerator valuesEnum = values.GetEnumerator(); - dictionary.Add(GetNewKey(dictionary), CreateTValue(3432)); - if (IDictionary_Generic_Keys_Values_Enumeration_ThrowsInvalidOperation_WhenParentModified) - { - Assert.Throws(() => valuesEnum.MoveNext()); - Assert.Throws(() => valuesEnum.Reset()); - } - else + if (!IsReadOnly) { - if (valuesEnum.MoveNext()) + IDictionary dictionary = GenericIDictionaryFactory(count); + ICollection values = dictionary.Values; + IEnumerator valuesEnum = values.GetEnumerator(); + dictionary.Add(GetNewKey(dictionary), CreateTValue(3432)); + if (IDictionary_Generic_Keys_Values_Enumeration_ThrowsInvalidOperation_WhenParentModified) { - _ = valuesEnum.Current; + Assert.Throws(() => valuesEnum.MoveNext()); + Assert.Throws(() => valuesEnum.Reset()); + } + else + { + if (valuesEnum.MoveNext()) + { + _ = valuesEnum.Current; + } + valuesEnum.Reset(); } - valuesEnum.Reset(); } } diff --git a/src/libraries/Common/tests/System/Collections/IDictionary.NonGeneric.Tests.cs b/src/libraries/Common/tests/System/Collections/IDictionary.NonGeneric.Tests.cs index f70229b28db38..578456c0481bb 100644 --- a/src/libraries/Common/tests/System/Collections/IDictionary.NonGeneric.Tests.cs +++ b/src/libraries/Common/tests/System/Collections/IDictionary.NonGeneric.Tests.cs @@ -94,6 +94,8 @@ protected object GetNewKey(IDictionary dictionary) /// protected virtual bool IDictionary_NonGeneric_Keys_Values_ParentDictionaryModifiedInvalidates => true; + protected virtual bool ExpectedIsFixedSize => false; + #endregion #region ICollection Helper Methods @@ -208,7 +210,7 @@ protected override IEnumerable GetModifyEnumerables(ModifyOper public void IDictionary_NonGeneric_IsFixedSize_Validity(int count) { IDictionary collection = NonGenericIDictionaryFactory(count); - Assert.False(collection.IsFixedSize); + Assert.Equal(ExpectedIsFixedSize, collection.IsFixedSize); } #endregion @@ -286,16 +288,19 @@ public void IDictionary_NonGeneric_ItemGet_PresenobjectReturnsCorrecobject(int c [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_NonGeneric_ItemSet_NullKey(int count) { - IDictionary dictionary = NonGenericIDictionaryFactory(count); - if (!NullAllowed) - { - Assert.Throws(() => dictionary[null] = CreateTValue(3)); - } - else + if (!IsReadOnly) { - object value = CreateTValue(3452); - dictionary[null] = value; - Assert.Equal(value, dictionary[null]); + IDictionary dictionary = NonGenericIDictionaryFactory(count); + if (!NullAllowed) + { + Assert.Throws(() => dictionary[null] = CreateTValue(3)); + } + else + { + object value = CreateTValue(3452); + dictionary[null] = value; + Assert.Equal(value, dictionary[null]); + } } } @@ -315,23 +320,29 @@ public void IDictionary_NonGeneric_ItemSet_OnReadOnlyDictionary_ThrowsNotSupport [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_NonGeneric_ItemSet_AddsNewValueWhenNotPresent(int count) { - IDictionary dictionary = NonGenericIDictionaryFactory(count); - object missingKey = GetNewKey(dictionary); - dictionary[missingKey] = CreateTValue(543); - Assert.Equal(count + 1, dictionary.Count); + if (!IsReadOnly) + { + IDictionary dictionary = NonGenericIDictionaryFactory(count); + object missingKey = GetNewKey(dictionary); + dictionary[missingKey] = CreateTValue(543); + Assert.Equal(count + 1, dictionary.Count); + } } [Theory] [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_NonGeneric_ItemSet_ReplacesExistingValueWhenPresent(int count) { - IDictionary dictionary = NonGenericIDictionaryFactory(count); - object existingKey = GetNewKey(dictionary); - dictionary.Add(existingKey, CreateTValue(5342)); - object newValue = CreateTValue(1234); - dictionary[existingKey] = newValue; - Assert.Equal(count + 1, dictionary.Count); - Assert.Equal(newValue, dictionary[existingKey]); + if (!IsReadOnly) + { + IDictionary dictionary = NonGenericIDictionaryFactory(count); + object existingKey = GetNewKey(dictionary); + dictionary.Add(existingKey, CreateTValue(5342)); + object newValue = CreateTValue(1234); + dictionary[existingKey] = newValue; + Assert.Equal(count + 1, dictionary.Count); + Assert.Equal(newValue, dictionary[existingKey]); + } } #endregion @@ -354,17 +365,20 @@ public void IDictionary_NonGeneric_Keys_ContainsAllCorrectobjects(int count) [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_NonGeneric_Keys_ModifyingTheDictionaryUpdatesTheCollection(int count) { - IDictionary dictionary = NonGenericIDictionaryFactory(count); - ICollection keys = dictionary.Keys; - int previousCount = keys.Count; - dictionary.Clear(); - if (IDictionary_NonGeneric_Keys_Values_ModifyingTheDictionaryUpdatesTheCollection) - { - Assert.Empty(keys); - } - else + if (!IsReadOnly) { - Assert.Equal(previousCount, keys.Count); + IDictionary dictionary = NonGenericIDictionaryFactory(count); + ICollection keys = dictionary.Keys; + int previousCount = keys.Count; + dictionary.Clear(); + if (IDictionary_NonGeneric_Keys_Values_ModifyingTheDictionaryUpdatesTheCollection) + { + Assert.Empty(keys); + } + else + { + Assert.Equal(previousCount, keys.Count); + } } } @@ -372,23 +386,26 @@ public void IDictionary_NonGeneric_Keys_ModifyingTheDictionaryUpdatesTheCollecti [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_NonGeneric_Keys_Enumeration_ParentDictionaryModifiedInvalidatesEnumerator(int count) { - IDictionary dictionary = NonGenericIDictionaryFactory(count); - ICollection keys = dictionary.Keys; - IEnumerator keysEnum = keys.GetEnumerator(); - dictionary.Add(GetNewKey(dictionary), CreateTValue(3432)); - if (IDictionary_NonGeneric_Keys_Values_ParentDictionaryModifiedInvalidates) - { - Assert.Throws(() => keysEnum.MoveNext()); - Assert.Throws(() => keysEnum.Reset()); - } - else + if (!IsReadOnly) { - keysEnum.MoveNext(); - if (count > 0) + IDictionary dictionary = NonGenericIDictionaryFactory(count); + ICollection keys = dictionary.Keys; + IEnumerator keysEnum = keys.GetEnumerator(); + dictionary.Add(GetNewKey(dictionary), CreateTValue(3432)); + if (IDictionary_NonGeneric_Keys_Values_ParentDictionaryModifiedInvalidates) { - var cur = keysEnum.Current; + Assert.Throws(() => keysEnum.MoveNext()); + Assert.Throws(() => keysEnum.Reset()); + } + else + { + keysEnum.MoveNext(); + if (count > 0) + { + var cur = keysEnum.Current; + } + keysEnum.Reset(); } - keysEnum.Reset(); } } @@ -423,34 +440,40 @@ public void IDictionary_NonGeneric_Values_ContainsAllCorrecobjects(int count) [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_NonGeneric_Values_IncludeDuplicatesMultipleTimes(int count) { - IDictionary dictionary = NonGenericIDictionaryFactory(count); - List entries = new List(); - - foreach (DictionaryEntry pair in dictionary) - entries.Add(pair); - foreach (DictionaryEntry pair in entries) + if (!IsReadOnly) { - object missingKey = GetNewKey(dictionary); - dictionary.Add(missingKey, (pair.Value)); + IDictionary dictionary = NonGenericIDictionaryFactory(count); + List entries = new List(); + + foreach (DictionaryEntry pair in dictionary) + entries.Add(pair); + foreach (DictionaryEntry pair in entries) + { + object missingKey = GetNewKey(dictionary); + dictionary.Add(missingKey, (pair.Value)); + } + Assert.Equal(count * 2, dictionary.Values.Count); } - Assert.Equal(count * 2, dictionary.Values.Count); } [Theory] [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_NonGeneric_Values_ModifyingTheDictionaryUpdatesTheCollection(int count) { - IDictionary dictionary = NonGenericIDictionaryFactory(count); - ICollection values = dictionary.Values; - int previousCount = values.Count; - dictionary.Clear(); - if (IDictionary_NonGeneric_Keys_Values_ModifyingTheDictionaryUpdatesTheCollection) - { - Assert.Empty(values); - } - else + if (!IsReadOnly) { - Assert.Equal(previousCount, values.Count); + IDictionary dictionary = NonGenericIDictionaryFactory(count); + ICollection values = dictionary.Values; + int previousCount = values.Count; + dictionary.Clear(); + if (IDictionary_NonGeneric_Keys_Values_ModifyingTheDictionaryUpdatesTheCollection) + { + Assert.Empty(values); + } + else + { + Assert.Equal(previousCount, values.Count); + } } } @@ -458,24 +481,27 @@ public void IDictionary_NonGeneric_Values_ModifyingTheDictionaryUpdatesTheCollec [MemberData(nameof(ValidCollectionSizes))] public virtual void IDictionary_NonGeneric_Values_Enumeration_ParentDictionaryModifiedInvalidatesEnumerator(int count) { - IDictionary dictionary = NonGenericIDictionaryFactory(count); - ICollection values = dictionary.Values; - IEnumerator valuesEnum = values.GetEnumerator(); - dictionary.Add(GetNewKey(dictionary), CreateTValue(3432)); - if (IDictionary_NonGeneric_Keys_Values_ParentDictionaryModifiedInvalidates) - { - Assert.Throws(() => valuesEnum.MoveNext()); - Assert.Throws(() => valuesEnum.Reset()); - Assert.Throws(() => valuesEnum.Current); - } - else + if (!IsReadOnly) { - valuesEnum.MoveNext(); - if (count > 0) + IDictionary dictionary = NonGenericIDictionaryFactory(count); + ICollection values = dictionary.Values; + IEnumerator valuesEnum = values.GetEnumerator(); + dictionary.Add(GetNewKey(dictionary), CreateTValue(3432)); + if (IDictionary_NonGeneric_Keys_Values_ParentDictionaryModifiedInvalidates) + { + Assert.Throws(() => valuesEnum.MoveNext()); + Assert.Throws(() => valuesEnum.Reset()); + Assert.Throws(() => valuesEnum.Current); + } + else { - var cur = valuesEnum.Current; + valuesEnum.MoveNext(); + if (count > 0) + { + var cur = valuesEnum.Current; + } + valuesEnum.Reset(); } - valuesEnum.Reset(); } } @@ -595,16 +621,34 @@ public void IDictionary_NonGeneric_Add_DuplicateKey(int count) public void IDictionary_NonGeneric_Remove_NullKey(int count) { IDictionary dictionary = NonGenericIDictionaryFactory(count); - if (!NullAllowed) + if (!IsReadOnly) { - Assert.Throws(() => dictionary.Remove(null)); + if (!NullAllowed) + { + Assert.Throws(() => dictionary.Remove(null)); + } + else + { + object value = CreateTValue(3452); + dictionary.Add(null, value); + dictionary.Remove(null); + Assert.Null(dictionary[null]); + } } - else + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IDictionary_NonGeneric_Remove_ThrowsWhenNotSupported(int count) + { + if (IsReadOnly) { - object value = CreateTValue(3452); - dictionary.Add(null, value); - dictionary.Remove(null); - Assert.Null(dictionary[null]); + IDictionary dictionary = NonGenericIDictionaryFactory(count); + foreach (DictionaryEntry entry in dictionary) + { + Assert.Throws(() => dictionary.Remove(entry.Key)); + break; + } } } diff --git a/src/libraries/Common/tests/System/Collections/ISet.Generic.Tests.cs b/src/libraries/Common/tests/System/Collections/ISet.Generic.Tests.cs index 1cc12e98dd405..9c0b36df38eeb 100644 --- a/src/libraries/Common/tests/System/Collections/ISet.Generic.Tests.cs +++ b/src/libraries/Common/tests/System/Collections/ISet.Generic.Tests.cs @@ -302,34 +302,51 @@ private void Validate_UnionWith(ISet set, IEnumerable enumerable) public void ISet_Generic_NullEnumerableArgument(int count) { ISet set = GenericISetFactory(count); - Assert.Throws(() => set.ExceptWith(null)); - Assert.Throws(() => set.IntersectWith(null)); Assert.Throws(() => set.IsProperSubsetOf(null)); Assert.Throws(() => set.IsProperSupersetOf(null)); Assert.Throws(() => set.IsSubsetOf(null)); Assert.Throws(() => set.IsSupersetOf(null)); Assert.Throws(() => set.Overlaps(null)); Assert.Throws(() => set.SetEquals(null)); - Assert.Throws(() => set.SymmetricExceptWith(null)); - Assert.Throws(() => set.UnionWith(null)); + if (!IsReadOnly) + { + Assert.Throws(() => set.ExceptWith(null)); + Assert.Throws(() => set.IntersectWith(null)); + Assert.Throws(() => set.SymmetricExceptWith(null)); + Assert.Throws(() => set.UnionWith(null)); + } + else + { + Assert.Throws(() => set.Add(CreateT(0))); + Assert.Throws(() => set.ExceptWith(null)); + Assert.Throws(() => set.IntersectWith(null)); + Assert.Throws(() => set.SymmetricExceptWith(null)); + Assert.Throws(() => set.UnionWith(null)); + } } [Theory] [MemberData(nameof(EnumerableTestData))] public void ISet_Generic_ExceptWith(EnumerableType enumerableType, int setLength, int enumerableLength, int numberOfMatchingElements, int numberOfDuplicateElements) { - ISet set = GenericISetFactory(setLength); - IEnumerable enumerable = CreateEnumerable(enumerableType, set, enumerableLength, numberOfMatchingElements, numberOfDuplicateElements); - Validate_ExceptWith(set, enumerable); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(setLength); + IEnumerable enumerable = CreateEnumerable(enumerableType, set, enumerableLength, numberOfMatchingElements, numberOfDuplicateElements); + Validate_ExceptWith(set, enumerable); + } } [Theory] [MemberData(nameof(EnumerableTestData))] public void ISet_Generic_IntersectWith(EnumerableType enumerableType, int setLength, int enumerableLength, int numberOfMatchingElements, int numberOfDuplicateElements) { - ISet set = GenericISetFactory(setLength); - IEnumerable enumerable = CreateEnumerable(enumerableType, set, enumerableLength, numberOfMatchingElements, numberOfDuplicateElements); - Validate_IntersectWith(set, enumerable); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(setLength); + IEnumerable enumerable = CreateEnumerable(enumerableType, set, enumerableLength, numberOfMatchingElements, numberOfDuplicateElements); + Validate_IntersectWith(set, enumerable); + } } [Theory] @@ -390,18 +407,24 @@ public void ISet_Generic_SetEquals(EnumerableType enumerableType, int setLength, [MemberData(nameof(EnumerableTestData))] public void ISet_Generic_SymmetricExceptWith(EnumerableType enumerableType, int setLength, int enumerableLength, int numberOfMatchingElements, int numberOfDuplicateElements) { - ISet set = GenericISetFactory(setLength); - IEnumerable enumerable = CreateEnumerable(enumerableType, set, enumerableLength, numberOfMatchingElements, numberOfDuplicateElements); - Validate_SymmetricExceptWith(set, enumerable); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(setLength); + IEnumerable enumerable = CreateEnumerable(enumerableType, set, enumerableLength, numberOfMatchingElements, numberOfDuplicateElements); + Validate_SymmetricExceptWith(set, enumerable); + } } [Theory] [MemberData(nameof(EnumerableTestData))] public void ISet_Generic_UnionWith(EnumerableType enumerableType, int setLength, int enumerableLength, int numberOfMatchingElements, int numberOfDuplicateElements) { - ISet set = GenericISetFactory(setLength); - IEnumerable enumerable = CreateEnumerable(enumerableType, set, enumerableLength, numberOfMatchingElements, numberOfDuplicateElements); - Validate_UnionWith(set, enumerable); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(setLength); + IEnumerable enumerable = CreateEnumerable(enumerableType, set, enumerableLength, numberOfMatchingElements, numberOfDuplicateElements); + Validate_UnionWith(set, enumerable); + } } #endregion @@ -412,8 +435,11 @@ public void ISet_Generic_UnionWith(EnumerableType enumerableType, int setLength, [MemberData(nameof(ValidCollectionSizes))] public void ISet_Generic_ExceptWith_Itself(int setLength) { - ISet set = GenericISetFactory(setLength); - Validate_ExceptWith(set, set); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(setLength); + Validate_ExceptWith(set, set); + } } [Theory] @@ -421,8 +447,11 @@ public void ISet_Generic_ExceptWith_Itself(int setLength) [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework, ".NET Framework throws InvalidOperationException")] public void ISet_Generic_IntersectWith_Itself(int setLength) { - ISet set = GenericISetFactory(setLength); - Validate_IntersectWith(set, set); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(setLength); + Validate_IntersectWith(set, set); + } } [Theory] @@ -477,16 +506,22 @@ public void ISet_Generic_SetEquals_Itself(int setLength) [MemberData(nameof(ValidCollectionSizes))] public void ISet_Generic_SymmetricExceptWith_Itself(int setLength) { - ISet set = GenericISetFactory(setLength); - Validate_SymmetricExceptWith(set, set); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(setLength); + Validate_SymmetricExceptWith(set, set); + } } [Theory] [MemberData(nameof(ValidCollectionSizes))] public void ISet_Generic_UnionWith_Itself(int setLength) { - ISet set = GenericISetFactory(setLength); - Validate_UnionWith(set, set); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(setLength); + Validate_UnionWith(set, set); + } } #endregion @@ -497,18 +532,24 @@ public void ISet_Generic_UnionWith_Itself(int setLength) [OuterLoop] public void ISet_Generic_ExceptWith_LargeSet() { - ISet set = GenericISetFactory(ISet_Large_Capacity); - IEnumerable enumerable = CreateEnumerable(EnumerableType.List, set, 150, 0, 0); - Validate_ExceptWith(set, enumerable); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(ISet_Large_Capacity); + IEnumerable enumerable = CreateEnumerable(EnumerableType.List, set, 150, 0, 0); + Validate_ExceptWith(set, enumerable); + } } [Fact] [OuterLoop] public void ISet_Generic_IntersectWith_LargeSet() { - ISet set = GenericISetFactory(ISet_Large_Capacity); - IEnumerable enumerable = CreateEnumerable(EnumerableType.List, set, 150, 0, 0); - Validate_IntersectWith(set, enumerable); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(ISet_Large_Capacity); + IEnumerable enumerable = CreateEnumerable(EnumerableType.List, set, 150, 0, 0); + Validate_IntersectWith(set, enumerable); + } } [Fact] @@ -569,18 +610,24 @@ public void ISet_Generic_SetEquals_LargeSet() [OuterLoop] public void ISet_Generic_SymmetricExceptWith_LargeSet() { - ISet set = GenericISetFactory(ISet_Large_Capacity); - IEnumerable enumerable = CreateEnumerable(EnumerableType.List, set, 150, 0, 0); - Validate_SymmetricExceptWith(set, enumerable); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(ISet_Large_Capacity); + IEnumerable enumerable = CreateEnumerable(EnumerableType.List, set, 150, 0, 0); + Validate_SymmetricExceptWith(set, enumerable); + } } [Fact] [OuterLoop] public void ISet_Generic_UnionWith_LargeSet() { - ISet set = GenericISetFactory(ISet_Large_Capacity); - IEnumerable enumerable = CreateEnumerable(EnumerableType.List, set, 150, 0, 0); - Validate_UnionWith(set, enumerable); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(ISet_Large_Capacity); + IEnumerable enumerable = CreateEnumerable(EnumerableType.List, set, 150, 0, 0); + Validate_UnionWith(set, enumerable); + } } #endregion @@ -591,25 +638,28 @@ public void ISet_Generic_UnionWith_LargeSet() [MemberData(nameof(EnumerableTestData))] public void ISet_Generic_SymmetricExceptWith_AfterRemovingElements(EnumerableType enumerableType, int setLength, int enumerableLength, int numberOfMatchingElements, int numberOfDuplicateElements) { - ISet set = GenericISetFactory(setLength); - T value = CreateT(532); - if (!set.Contains(value)) - set.Add(value); - set.Remove(value); - IEnumerable enumerable = CreateEnumerable(enumerableType, set, enumerableLength, numberOfMatchingElements, numberOfDuplicateElements); - Debug.Assert(enumerable != null); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(setLength); + T value = CreateT(532); + if (!set.Contains(value)) + set.Add(value); + set.Remove(value); + IEnumerable enumerable = CreateEnumerable(enumerableType, set, enumerableLength, numberOfMatchingElements, numberOfDuplicateElements); + Debug.Assert(enumerable != null); - IEqualityComparer comparer = GetIEqualityComparer(); - HashSet expected = new HashSet(comparer); - foreach (T element in enumerable) - if (!set.Contains(element, comparer)) - expected.Add(element); - foreach (T element in set) - if (!enumerable.Contains(element, comparer)) - expected.Add(element); - set.SymmetricExceptWith(enumerable); - Assert.Equal(expected.Count, set.Count); - Assert.True(expected.SetEquals(set)); + IEqualityComparer comparer = GetIEqualityComparer(); + HashSet expected = new HashSet(comparer); + foreach (T element in enumerable) + if (!set.Contains(element, comparer)) + expected.Add(element); + foreach (T element in set) + if (!enumerable.Contains(element, comparer)) + expected.Add(element); + set.SymmetricExceptWith(enumerable); + Assert.Equal(expected.Count, set.Count); + Assert.True(expected.SetEquals(set)); + } } #endregion diff --git a/src/libraries/System.Collections.Immutable/ref/Stubs.cs b/src/libraries/System.Collections.Immutable/ref/Stubs.cs deleted file mode 100644 index 58f51cc786943..0000000000000 --- a/src/libraries/System.Collections.Immutable/ref/Stubs.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.ComponentModel; -using System.Runtime.CompilerServices; - -namespace System.Collections.Generic -{ -#if !NET5_0_OR_GREATER - /// - /// Provides a readonly abstraction of a set. - /// - /// The type of elements in the set. - public interface IReadOnlySet : IReadOnlyCollection // TODO: fix this - { - /// - /// Determines if the set contains a specific item - /// - /// The item to check if the set contains. - /// if found; otherwise . - bool Contains(T item); - - /// - /// Determines whether the current set is a proper (strict) subset of a specified collection. - /// - /// The collection to compare to the current set. - /// if the current set is a proper subset of other; otherwise . - /// other is . - bool IsProperSubsetOf(IEnumerable other); - - /// - /// Determines whether the current set is a proper (strict) superset of a specified collection. - /// - /// The collection to compare to the current set. - /// if the collection is a proper superset of other; otherwise . - /// other is . - bool IsProperSupersetOf(IEnumerable other); - - /// - /// Determine whether the current set is a subset of a specified collection. - /// - /// The collection to compare to the current set. - /// if the current set is a subset of other; otherwise . - /// other is . - bool IsSubsetOf(IEnumerable other); - - /// - /// Determine whether the current set is a super set of a specified collection. - /// - /// The collection to compare to the current set - /// if the current set is a subset of other; otherwise . - /// other is . - bool IsSupersetOf(IEnumerable other); - - /// - /// Determines whether the current set overlaps with the specified collection. - /// - /// The collection to compare to the current set. - /// if the current set and other share at least one common element; otherwise, . - /// other is . - bool Overlaps(IEnumerable other); - - /// - /// Determines whether the current set and the specified collection contain the same elements. - /// - /// The collection to compare to the current set. - /// if the current set is equal to other; otherwise, . - /// other is . - bool SetEquals(IEnumerable other); - } -#endif -} diff --git a/src/libraries/System.Collections.Immutable/ref/System.Collections.Immutable.cs b/src/libraries/System.Collections.Immutable/ref/System.Collections.Immutable.cs index 5e9a1ab2643b8..add568dd56272 100644 --- a/src/libraries/System.Collections.Immutable/ref/System.Collections.Immutable.cs +++ b/src/libraries/System.Collections.Immutable/ref/System.Collections.Immutable.cs @@ -4,310 +4,121 @@ // Changes to this file must follow the https://aka.ms/api-review process. // ------------------------------------------------------------------------------ -namespace System.Collections.Immutable +namespace System.Collections.Frozen { - public static partial class Freezer + public static partial class FrozenDictionary { - public static System.Collections.Immutable.FrozenIntDictionary ToFrozenDictionary(this System.Collections.Generic.IEnumerable>? pairs) { throw null; } - public static System.Collections.Immutable.FrozenOrdinalStringDictionary ToFrozenDictionary(this System.Collections.Generic.IEnumerable>? pairs) { throw null; } - public static System.Collections.Immutable.FrozenOrdinalStringDictionary ToFrozenDictionary(this System.Collections.Generic.IEnumerable>? pairs, bool ignoreCase) { throw null; } - public static System.Collections.Immutable.FrozenDictionary ToFrozenDictionary(this System.Collections.Generic.IEnumerable>? pairs) where TKey : notnull { throw null; } - public static System.Collections.Immutable.FrozenDictionary ToFrozenDictionary(this System.Collections.Generic.IEnumerable>? pairs, System.Collections.Generic.IEqualityComparer comparer) where TKey : notnull { throw null; } - public static System.Collections.Immutable.FrozenList ToFrozenList(this System.Collections.Generic.IEnumerable? items) { throw null; } - public static System.Collections.Immutable.FrozenIntSet ToFrozenSet(this System.Collections.Generic.IEnumerable? items) { throw null; } - public static System.Collections.Immutable.FrozenOrdinalStringSet ToFrozenSet(this System.Collections.Generic.IEnumerable? items) { throw null; } - public static System.Collections.Immutable.FrozenOrdinalStringSet ToFrozenSet(this System.Collections.Generic.IEnumerable? items, bool ignoreCase) { throw null; } - public static System.Collections.Immutable.FrozenSet ToFrozenSet(this System.Collections.Generic.IEnumerable? items) where T : notnull { throw null; } - public static System.Collections.Immutable.FrozenSet ToFrozenSet(this System.Collections.Generic.IEnumerable? items, System.Collections.Generic.IEqualityComparer comparer) where T : notnull { throw null; } + public static System.Collections.Frozen.FrozenDictionary ToFrozenDictionary(this System.Collections.Generic.IEnumerable> source, System.Collections.Generic.IEqualityComparer? comparer = null) where TKey : notnull { throw null; } + public static System.Collections.Frozen.FrozenDictionary ToFrozenDictionary(this System.Collections.Generic.IEnumerable source, System.Func keySelector, System.Collections.Generic.IEqualityComparer? comparer = null) where TKey : notnull { throw null; } + public static System.Collections.Frozen.FrozenDictionary ToFrozenDictionary(this System.Collections.Generic.IEnumerable source, System.Func keySelector, System.Func elementSelector, System.Collections.Generic.IEqualityComparer? comparer = null) where TKey : notnull { throw null; } } - public readonly partial struct FrozenDictionary : System.Collections.Generic.ICollection>, System.Collections.Generic.IDictionary, System.Collections.Generic.IEnumerable>, System.Collections.Immutable.IFrozenDictionary, System.Collections.Generic.IReadOnlyCollection>, System.Collections.Generic.IReadOnlyDictionary, System.Collections.IEnumerable where TKey : notnull + public abstract partial class FrozenDictionary : System.Collections.Generic.ICollection>, System.Collections.Generic.IDictionary, System.Collections.Generic.IEnumerable>, System.Collections.Generic.IReadOnlyCollection>, System.Collections.Generic.IReadOnlyDictionary, System.Collections.ICollection, System.Collections.IDictionary, System.Collections.IEnumerable where TKey : notnull { - private readonly TKey[] _keys; - private readonly TValue[] _values; - private readonly object _dummy; - private readonly int _dummyPrimitive; + internal FrozenDictionary() { } public System.Collections.Generic.IEqualityComparer Comparer { get { throw null; } } public int Count { get { throw null; } } - public static System.Collections.Immutable.FrozenDictionary Empty { get { throw null; } } - public TValue this[TKey key] { get { throw null; } } - public System.Collections.Immutable.FrozenList Keys { get { throw null; } } + public static System.Collections.Frozen.FrozenDictionary Empty { get { throw null; } } + public ref readonly TValue this[TKey key] { get { throw null; } } + public System.Collections.Immutable.ImmutableArray Keys { get { throw null; } } bool System.Collections.Generic.ICollection>.IsReadOnly { get { throw null; } } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] TValue System.Collections.Generic.IDictionary.this[TKey key] { get { throw null; } set { } } System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Keys { get { throw null; } } System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Values { get { throw null; } } + TValue System.Collections.Generic.IReadOnlyDictionary.this[TKey key] { get { throw null; } } System.Collections.Generic.IEnumerable System.Collections.Generic.IReadOnlyDictionary.Keys { get { throw null; } } System.Collections.Generic.IEnumerable System.Collections.Generic.IReadOnlyDictionary.Values { get { throw null; } } - public System.Collections.Immutable.FrozenList Values { get { throw null; } } + bool System.Collections.ICollection.IsSynchronized { get { throw null; } } + object System.Collections.ICollection.SyncRoot { get { throw null; } } + bool System.Collections.IDictionary.IsFixedSize { get { throw null; } } + bool System.Collections.IDictionary.IsReadOnly { get { throw null; } } + object? System.Collections.IDictionary.this[object key] { get { throw null; } set { } } + System.Collections.ICollection System.Collections.IDictionary.Keys { get { throw null; } } + System.Collections.ICollection System.Collections.IDictionary.Values { get { throw null; } } + public System.Collections.Immutable.ImmutableArray Values { get { throw null; } } public bool ContainsKey(TKey key) { throw null; } - public void CopyTo(System.Collections.Generic.KeyValuePair[] array, int arrayIndex) { } + public void CopyTo(System.Collections.Generic.KeyValuePair[] destination, int destinationIndex) { } public void CopyTo(System.Span> destination) { } - public ref readonly TValue GetByRef(TKey key) { throw null; } - public System.Collections.Immutable.FrozenPairEnumerator GetEnumerator() { throw null; } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public System.Collections.Frozen.FrozenDictionary.Enumerator GetEnumerator() { throw null; } + public ref readonly TValue GetValueRefOrNullRef(TKey key) { throw null; } void System.Collections.Generic.ICollection>.Add(System.Collections.Generic.KeyValuePair item) { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] void System.Collections.Generic.ICollection>.Clear() { } bool System.Collections.Generic.ICollection>.Contains(System.Collections.Generic.KeyValuePair item) { throw null; } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] bool System.Collections.Generic.ICollection>.Remove(System.Collections.Generic.KeyValuePair item) { throw null; } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] void System.Collections.Generic.IDictionary.Add(TKey key, TValue value) { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] bool System.Collections.Generic.IDictionary.Remove(TKey key) { throw null; } System.Collections.Generic.IEnumerator> System.Collections.Generic.IEnumerable>.GetEnumerator() { throw null; } + void System.Collections.ICollection.CopyTo(System.Array array, int index) { } + void System.Collections.IDictionary.Add(object key, object? value) { } + void System.Collections.IDictionary.Clear() { } + bool System.Collections.IDictionary.Contains(object key) { throw null; } + System.Collections.IDictionaryEnumerator System.Collections.IDictionary.GetEnumerator() { throw null; } + void System.Collections.IDictionary.Remove(object key) { } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } - public ref readonly TValue TryGetByRef(TKey key) { throw null; } public bool TryGetValue(TKey key, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TValue value) { throw null; } + public partial struct Enumerator : System.Collections.Generic.IEnumerator>, System.Collections.IEnumerator, System.IDisposable + { + private readonly TKey[] _keys; + private readonly TValue[] _values; + private object _dummy; + private int _dummyPrimitive; + public readonly System.Collections.Generic.KeyValuePair Current { get { throw null; } } + object System.Collections.IEnumerator.Current { get { throw null; } } + public bool MoveNext() { throw null; } + void System.Collections.IEnumerator.Reset() { } + void System.IDisposable.Dispose() { } + } } - public partial struct FrozenEnumerator : System.Collections.Generic.IEnumerator, System.Collections.IEnumerator, System.IDisposable - { - private readonly T[] _entries; - private object _dummy; - private int _dummyPrimitive; - public readonly T Current { get { throw null; } } - object System.Collections.IEnumerator.Current { get { throw null; } } - public bool MoveNext() { throw null; } - void System.Collections.IEnumerator.Reset() { } - void System.IDisposable.Dispose() { } - } - public readonly partial struct FrozenIntDictionary : System.Collections.Generic.ICollection>, System.Collections.Generic.IDictionary, System.Collections.Generic.IEnumerable>, System.Collections.Immutable.IFrozenDictionary, System.Collections.Generic.IReadOnlyCollection>, System.Collections.Generic.IReadOnlyDictionary, System.Collections.IEnumerable - { - private readonly TValue[] _values; - private readonly object _dummy; - private readonly int _dummyPrimitive; - public int Count { get { throw null; } } - public static System.Collections.Immutable.FrozenIntDictionary Empty { get { throw null; } } - public TValue this[int key] { get { throw null; } } - public System.Collections.Immutable.FrozenList Keys { get { throw null; } } - bool System.Collections.Generic.ICollection>.IsReadOnly { get { throw null; } } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - TValue System.Collections.Generic.IDictionary.this[int key] { get { throw null; } set { } } - System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Keys { get { throw null; } } - System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Values { get { throw null; } } - System.Collections.Generic.IEnumerable System.Collections.Generic.IReadOnlyDictionary.Keys { get { throw null; } } - System.Collections.Generic.IEnumerable System.Collections.Generic.IReadOnlyDictionary.Values { get { throw null; } } - public System.Collections.Immutable.FrozenList Values { get { throw null; } } - public bool ContainsKey(int key) { throw null; } - public void CopyTo(System.Collections.Generic.KeyValuePair[] array, int arrayIndex) { } - public void CopyTo(System.Span> destination) { } - public ref readonly TValue GetByRef(int key) { throw null; } - public System.Collections.Immutable.FrozenPairEnumerator GetEnumerator() { throw null; } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - void System.Collections.Generic.ICollection>.Add(System.Collections.Generic.KeyValuePair item) { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - void System.Collections.Generic.ICollection>.Clear() { } - bool System.Collections.Generic.ICollection>.Contains(System.Collections.Generic.KeyValuePair item) { throw null; } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - bool System.Collections.Generic.ICollection>.Remove(System.Collections.Generic.KeyValuePair item) { throw null; } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - void System.Collections.Generic.IDictionary.Add(int key, TValue value) { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - bool System.Collections.Generic.IDictionary.Remove(int key) { throw null; } - System.Collections.Generic.IEnumerator> System.Collections.Generic.IEnumerable>.GetEnumerator() { throw null; } - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } - public ref readonly TValue TryGetByRef(int key) { throw null; } - public bool TryGetValue(int key, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TValue value) { throw null; } - } - public readonly partial struct FrozenIntSet : System.Collections.Generic.ICollection, System.Collections.Generic.IEnumerable, System.Collections.Immutable.IFrozenSet, System.Collections.Generic.IReadOnlyCollection, System.Collections.Generic.IReadOnlySet, System.Collections.Generic.ISet, System.Collections.IEnumerable - { - private readonly object _dummy; - private readonly int _dummyPrimitive; - public int Count { get { throw null; } } - public static System.Collections.Immutable.FrozenIntSet Empty { get { throw null; } } - public System.Collections.Immutable.FrozenList Items { get { throw null; } } - bool System.Collections.Generic.ICollection.IsReadOnly { get { throw null; } } - public bool Contains(int item) { throw null; } - public void CopyTo(int[] array, int arrayIndex) { } - public void CopyTo(System.Span destination) { } - public System.Collections.Immutable.FrozenEnumerator GetEnumerator() { throw null; } - public bool IsProperSubsetOf(System.Collections.Generic.IEnumerable other) { throw null; } - public bool IsProperSupersetOf(System.Collections.Generic.IEnumerable other) { throw null; } - public bool IsSubsetOf(System.Collections.Generic.IEnumerable other) { throw null; } - public bool IsSupersetOf(System.Collections.Generic.IEnumerable other) { throw null; } - public bool Overlaps(System.Collections.Generic.IEnumerable other) { throw null; } - public bool SetEquals(System.Collections.Generic.IEnumerable other) { throw null; } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - void System.Collections.Generic.ICollection.Add(int item) { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - void System.Collections.Generic.ICollection.Clear() { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - bool System.Collections.Generic.ICollection.Remove(int item) { throw null; } - System.Collections.Generic.IEnumerator System.Collections.Generic.IEnumerable.GetEnumerator() { throw null; } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - bool System.Collections.Generic.ISet.Add(int item) { throw null; } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - void System.Collections.Generic.ISet.ExceptWith(System.Collections.Generic.IEnumerable other) { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - void System.Collections.Generic.ISet.IntersectWith(System.Collections.Generic.IEnumerable other) { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - void System.Collections.Generic.ISet.SymmetricExceptWith(System.Collections.Generic.IEnumerable other) { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - void System.Collections.Generic.ISet.UnionWith(System.Collections.Generic.IEnumerable other) { } - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } - } - public readonly partial struct FrozenList : System.Collections.Generic.ICollection, System.Collections.Generic.IEnumerable, System.Collections.Generic.IReadOnlyCollection, System.Collections.Generic.IReadOnlyList, System.Collections.IEnumerable - { - private readonly T[] _items; - private readonly object _dummy; - private readonly int _dummyPrimitive; - public int Count { get { throw null; } } - public static System.Collections.Immutable.FrozenList Empty { get { throw null; } } - public T this[int index] { get { throw null; } } - bool System.Collections.Generic.ICollection.IsReadOnly { get { throw null; } } - public System.ReadOnlySpan AsSpan() { throw null; } - public bool Contains(T item) { throw null; } - public void CopyTo(System.Span destination) { } - public void CopyTo(T[] array, int arrayIndex) { } - public System.Collections.Immutable.FrozenEnumerator GetEnumerator() { throw null; } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - void System.Collections.Generic.ICollection.Add(T item) { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - void System.Collections.Generic.ICollection.Clear() { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - bool System.Collections.Generic.ICollection.Remove(T item) { throw null; } - System.Collections.Generic.IEnumerator System.Collections.Generic.IEnumerable.GetEnumerator() { throw null; } - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } - } - public readonly partial struct FrozenOrdinalStringDictionary : System.Collections.Generic.ICollection>, System.Collections.Generic.IDictionary, System.Collections.Generic.IEnumerable>, System.Collections.Immutable.IFrozenDictionary, System.Collections.Generic.IReadOnlyCollection>, System.Collections.Generic.IReadOnlyDictionary, System.Collections.IEnumerable - { - private readonly TValue[] _values; - private readonly object _dummy; - private readonly int _dummyPrimitive; - public int Count { get { throw null; } } - public static System.Collections.Immutable.FrozenOrdinalStringDictionary Empty { get { throw null; } } - public TValue this[string key] { get { throw null; } } - public System.Collections.Immutable.FrozenList Keys { get { throw null; } } - bool System.Collections.Generic.ICollection>.IsReadOnly { get { throw null; } } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - TValue System.Collections.Generic.IDictionary.this[string key] { get { throw null; } set { } } - System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Keys { get { throw null; } } - System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Values { get { throw null; } } - System.Collections.Generic.IEnumerable System.Collections.Generic.IReadOnlyDictionary.Keys { get { throw null; } } - System.Collections.Generic.IEnumerable System.Collections.Generic.IReadOnlyDictionary.Values { get { throw null; } } - public System.Collections.Immutable.FrozenList Values { get { throw null; } } - public bool ContainsKey(string key) { throw null; } - public void CopyTo(System.Collections.Generic.KeyValuePair[] array, int arrayIndex) { } - public void CopyTo(System.Span> destination) { } - public ref readonly TValue GetByRef(string key) { throw null; } - public System.Collections.Immutable.FrozenPairEnumerator GetEnumerator() { throw null; } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - void System.Collections.Generic.ICollection>.Add(System.Collections.Generic.KeyValuePair item) { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - void System.Collections.Generic.ICollection>.Clear() { } - bool System.Collections.Generic.ICollection>.Contains(System.Collections.Generic.KeyValuePair item) { throw null; } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - bool System.Collections.Generic.ICollection>.Remove(System.Collections.Generic.KeyValuePair item) { throw null; } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - void System.Collections.Generic.IDictionary.Add(string key, TValue value) { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - bool System.Collections.Generic.IDictionary.Remove(string key) { throw null; } - System.Collections.Generic.IEnumerator> System.Collections.Generic.IEnumerable>.GetEnumerator() { throw null; } - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } - public ref readonly TValue TryGetByRef(string key) { throw null; } - public bool TryGetValue(string key, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TValue value) { throw null; } - } - public readonly partial struct FrozenOrdinalStringSet : System.Collections.Generic.ICollection, System.Collections.Generic.IEnumerable, System.Collections.Immutable.IFrozenSet, System.Collections.Generic.IReadOnlyCollection, System.Collections.Generic.IReadOnlySet, System.Collections.Generic.ISet, System.Collections.IEnumerable - { - private readonly object _dummy; - private readonly int _dummyPrimitive; - public int Count { get { throw null; } } - public static System.Collections.Immutable.FrozenOrdinalStringSet Empty { get { throw null; } } - public System.Collections.Immutable.FrozenList Items { get { throw null; } } - bool System.Collections.Generic.ICollection.IsReadOnly { get { throw null; } } - public bool Contains(string item) { throw null; } - public void CopyTo(System.Span destination) { } - public void CopyTo(string[] array, int arrayIndex) { } - public System.Collections.Immutable.FrozenEnumerator GetEnumerator() { throw null; } - public bool IsProperSubsetOf(System.Collections.Generic.IEnumerable other) { throw null; } - public bool IsProperSupersetOf(System.Collections.Generic.IEnumerable other) { throw null; } - public bool IsSubsetOf(System.Collections.Generic.IEnumerable other) { throw null; } - public bool IsSupersetOf(System.Collections.Generic.IEnumerable other) { throw null; } - public bool Overlaps(System.Collections.Generic.IEnumerable other) { throw null; } - public bool SetEquals(System.Collections.Generic.IEnumerable other) { throw null; } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - void System.Collections.Generic.ICollection.Add(string item) { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - void System.Collections.Generic.ICollection.Clear() { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - bool System.Collections.Generic.ICollection.Remove(string item) { throw null; } - System.Collections.Generic.IEnumerator System.Collections.Generic.IEnumerable.GetEnumerator() { throw null; } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - bool System.Collections.Generic.ISet.Add(string item) { throw null; } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - void System.Collections.Generic.ISet.ExceptWith(System.Collections.Generic.IEnumerable other) { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - void System.Collections.Generic.ISet.IntersectWith(System.Collections.Generic.IEnumerable other) { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - void System.Collections.Generic.ISet.SymmetricExceptWith(System.Collections.Generic.IEnumerable other) { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - void System.Collections.Generic.ISet.UnionWith(System.Collections.Generic.IEnumerable other) { } - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } - } - public partial struct FrozenPairEnumerator : System.Collections.Generic.IEnumerator>, System.Collections.IEnumerator, System.IDisposable + public static partial class FrozenSet { - private readonly TKey[] _keys; - private readonly TValue[] _values; - private object _dummy; - private int _dummyPrimitive; - public readonly System.Collections.Generic.KeyValuePair Current { get { throw null; } } - object System.Collections.IEnumerator.Current { get { throw null; } } - public bool MoveNext() { throw null; } - void System.Collections.IEnumerator.Reset() { } - void System.IDisposable.Dispose() { } + public static System.Collections.Frozen.FrozenSet ToFrozenSet(this System.Collections.Generic.IEnumerable source, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } } - public readonly partial struct FrozenSet : System.Collections.Generic.ICollection, System.Collections.Generic.IEnumerable, System.Collections.Immutable.IFrozenSet, System.Collections.Generic.IReadOnlyCollection, System.Collections.Generic.IReadOnlySet, System.Collections.Generic.ISet, System.Collections.IEnumerable where T : notnull + public abstract partial class FrozenSet : System.Collections.Generic.ICollection, System.Collections.Generic.IEnumerable, System.Collections.Generic.IReadOnlyCollection, System.Collections.Generic.ISet, System.Collections.ICollection, System.Collections.IEnumerable { - private readonly T[] _items; - private readonly object _dummy; - private readonly int _dummyPrimitive; + internal FrozenSet() { } public System.Collections.Generic.IEqualityComparer Comparer { get { throw null; } } public int Count { get { throw null; } } - public static System.Collections.Immutable.FrozenSet Empty { get { throw null; } } - public System.Collections.Immutable.FrozenList Items { get { throw null; } } + public static System.Collections.Frozen.FrozenSet Empty { get { throw null; } } + public System.Collections.Immutable.ImmutableArray Items { get { throw null; } } bool System.Collections.Generic.ICollection.IsReadOnly { get { throw null; } } + bool System.Collections.ICollection.IsSynchronized { get { throw null; } } + object System.Collections.ICollection.SyncRoot { get { throw null; } } public bool Contains(T item) { throw null; } public void CopyTo(System.Span destination) { } - public void CopyTo(T[] array, int arrayIndex) { } - public System.Collections.Immutable.FrozenEnumerator GetEnumerator() { throw null; } + public void CopyTo(T[] destination, int destinationIndex) { } + public System.Collections.Frozen.FrozenSet.Enumerator GetEnumerator() { throw null; } public bool IsProperSubsetOf(System.Collections.Generic.IEnumerable other) { throw null; } public bool IsProperSupersetOf(System.Collections.Generic.IEnumerable other) { throw null; } public bool IsSubsetOf(System.Collections.Generic.IEnumerable other) { throw null; } public bool IsSupersetOf(System.Collections.Generic.IEnumerable other) { throw null; } public bool Overlaps(System.Collections.Generic.IEnumerable other) { throw null; } public bool SetEquals(System.Collections.Generic.IEnumerable other) { throw null; } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] void System.Collections.Generic.ICollection.Add(T item) { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] void System.Collections.Generic.ICollection.Clear() { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] bool System.Collections.Generic.ICollection.Remove(T item) { throw null; } System.Collections.Generic.IEnumerator System.Collections.Generic.IEnumerable.GetEnumerator() { throw null; } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] bool System.Collections.Generic.ISet.Add(T item) { throw null; } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] void System.Collections.Generic.ISet.ExceptWith(System.Collections.Generic.IEnumerable other) { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] void System.Collections.Generic.ISet.IntersectWith(System.Collections.Generic.IEnumerable other) { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] void System.Collections.Generic.ISet.SymmetricExceptWith(System.Collections.Generic.IEnumerable other) { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] void System.Collections.Generic.ISet.UnionWith(System.Collections.Generic.IEnumerable other) { } + void System.Collections.ICollection.CopyTo(System.Array array, int index) { } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + public bool TryGetValue(T equalValue, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out T actualValue) { throw null; } + public partial struct Enumerator : System.Collections.Generic.IEnumerator, System.Collections.IEnumerator, System.IDisposable + { + private readonly T[] _entries; + private object _dummy; + private int _dummyPrimitive; + public readonly T Current { get { throw null; } } + object System.Collections.IEnumerator.Current { get { throw null; } } + public bool MoveNext() { throw null; } + void System.Collections.IEnumerator.Reset() { } + void System.IDisposable.Dispose() { } + } } - public partial interface IFrozenDictionary : System.Collections.Generic.IEnumerable>, System.Collections.Generic.IReadOnlyCollection>, System.Collections.Generic.IReadOnlyDictionary, System.Collections.IEnumerable where TKey : notnull - { - new System.Collections.Immutable.FrozenList Keys { get; } - new System.Collections.Immutable.FrozenList Values { get; } - ref readonly TValue GetByRef(TKey key); - new System.Collections.Immutable.FrozenPairEnumerator GetEnumerator(); - ref readonly TValue TryGetByRef(TKey key); - } - public partial interface IFrozenSet : System.Collections.Generic.IEnumerable, System.Collections.Generic.IReadOnlyCollection, System.Collections.Generic.IReadOnlySet, System.Collections.IEnumerable where T : notnull - { - System.Collections.Immutable.FrozenList Items { get; } - new System.Collections.Immutable.FrozenEnumerator GetEnumerator(); - } +} +namespace System.Collections.Immutable +{ public partial interface IImmutableDictionary : System.Collections.Generic.IEnumerable>, System.Collections.Generic.IReadOnlyCollection>, System.Collections.Generic.IReadOnlyDictionary, System.Collections.IEnumerable { System.Collections.Immutable.IImmutableDictionary Add(TKey key, TValue value); diff --git a/src/libraries/System.Collections.Immutable/ref/System.Collections.Immutable.csproj b/src/libraries/System.Collections.Immutable/ref/System.Collections.Immutable.csproj index 3ce24a84550f7..574ba484fb7b1 100644 --- a/src/libraries/System.Collections.Immutable/ref/System.Collections.Immutable.csproj +++ b/src/libraries/System.Collections.Immutable/ref/System.Collections.Immutable.csproj @@ -4,7 +4,6 @@ - diff --git a/src/libraries/System.Collections.Immutable/ref/System.Collections.Immutable.netcoreapp.cs b/src/libraries/System.Collections.Immutable/ref/System.Collections.Immutable.netcoreapp.cs index d6c786d33e236..c419ecd91f218 100644 --- a/src/libraries/System.Collections.Immutable/ref/System.Collections.Immutable.netcoreapp.cs +++ b/src/libraries/System.Collections.Immutable/ref/System.Collections.Immutable.netcoreapp.cs @@ -4,6 +4,12 @@ // Changes to this file must follow the https://aka.ms/api-review process. // ------------------------------------------------------------------------------ +namespace System.Collections.Frozen +{ + public abstract partial class FrozenSet : System.Collections.Generic.IReadOnlySet + { + } +} namespace System.Collections.Immutable { public readonly partial struct ImmutableArray : System.Collections.Generic.ICollection, System.Collections.Generic.IEnumerable, System.Collections.Generic.IList, System.Collections.Generic.IReadOnlyCollection, System.Collections.Generic.IReadOnlyList, System.Collections.ICollection, System.Collections.IEnumerable, System.Collections.IList, System.Collections.Immutable.IImmutableList, System.Collections.IStructuralComparable, System.Collections.IStructuralEquatable, System.IEquatable> diff --git a/src/libraries/System.Collections.Immutable/src/Resources/Strings.resx b/src/libraries/System.Collections.Immutable/src/Resources/Strings.resx index c92a6d097a100..8f13478c75da2 100644 --- a/src/libraries/System.Collections.Immutable/src/Resources/Strings.resx +++ b/src/libraries/System.Collections.Immutable/src/Resources/Strings.resx @@ -87,4 +87,22 @@ This operation cannot be performed on a default instance of ImmutableArray<T>. Consider initializing the array, or checking the ImmutableArray<T>.IsDefault property. - \ No newline at end of file + + Hashtable's capacity overflowed and went negative. Check load factor, capacity and the current size of the table. + + + Only single dimensional arrays are supported for the requested action. + + + The lower bound of target array must be zero. + + + Destination array is not long enough to copy all the items in the collection. Check array index and length. + + + Target array type is not compatible with the type of items in the collection. + + + Non-negative number required. + + 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 f9da15034db78..c21fe6b637635 100644 --- a/src/libraries/System.Collections.Immutable/src/System.Collections.Immutable.csproj +++ b/src/libraries/System.Collections.Immutable/src/System.Collections.Immutable.csproj @@ -12,32 +12,32 @@ The System.Collections.Immutable library is built-in as part of the shared frame - + - + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + - @@ -125,11 +125,9 @@ 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/DefaultFrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/DefaultFrozenDictionary.cs new file mode 100644 index 0000000000000..cc4fd2dad7323 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/DefaultFrozenDictionary.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace System.Collections.Frozen +{ + /// Provides the default implementation to use when no other special-cases apply. + /// The type of the keys in the dictionary. + /// The type of the values in the dictionary. + internal sealed class DefaultFrozenDictionary : KeysAndValuesFrozenDictionary, IDictionary + where TKey : notnull + { + internal DefaultFrozenDictionary(Dictionary source, IEqualityComparer comparer) : + base(source, comparer) + { + } + + /// + private protected override ref readonly TValue GetValueRefOrNullRefCore(TKey key) + { + IEqualityComparer comparer = Comparer; + + int hashCode = comparer.GetHashCode(key); + _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); + + while (index <= endIndex) + { + if (hashCode == _hashTable.HashCodes[index]) + { + if (comparer.Equals(key, _keys[index])) + { + return ref _values[index]; + } + } + + index++; + } + + return ref Unsafe.NullRef(); + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/DefaultFrozenSet.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/DefaultFrozenSet.cs new file mode 100644 index 0000000000000..062573424eccd --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/DefaultFrozenSet.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace System.Collections.Frozen +{ + /// Provides the default implementation to use when no other special-cases apply. + /// The type of the values in the set. + internal sealed class DefaultFrozenSet : ItemsFrozenSet.GSW> + { + internal DefaultFrozenSet(HashSet source, IEqualityComparer comparer) : + base(source, comparer) + { + } + + /// + private protected override int FindItemIndex(T item) + { + IEqualityComparer comparer = Comparer; + + int hashCode = item is null ? 0 : comparer.GetHashCode(item); + _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); + + while (index <= endIndex) + { + if (hashCode == _hashTable.HashCodes[index]) + { + if (comparer.Equals(item, _items[index])) + { + return index; + } + } + + index++; + } + + return -1; + } + + internal struct GSW : IGenericSpecializedWrapper + { + private DefaultFrozenSet _set; + public void Store(FrozenSet set) => _set = (DefaultFrozenSet)set; + + public int Count => _set.Count; + public IEqualityComparer Comparer => _set.Comparer; + public int FindItemIndex(T item) => _set.FindItemIndex(item); + public Enumerator GetEnumerator() => _set.GetEnumerator(); + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/EmptyFrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/EmptyFrozenDictionary.cs new file mode 100644 index 0000000000000..5050ef9a90181 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/EmptyFrozenDictionary.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Runtime.CompilerServices; + +namespace System.Collections.Frozen +{ + /// Provides an empty to use when there are zero key/value pairs to be stored. + internal sealed class EmptyFrozenDictionary : FrozenDictionary + where TKey : notnull + { + public EmptyFrozenDictionary() : base(EqualityComparer.Default) { } + + /// + private protected override ImmutableArray KeysCore => ImmutableArray.Empty; + + /// + private protected override ImmutableArray ValuesCore => ImmutableArray.Empty; + + /// + private protected override Enumerator GetEnumeratorCore() => new Enumerator(Array.Empty(), Array.Empty()); + + /// + private protected override int CountCore => 0; + + /// + private protected override ref readonly TValue GetValueRefOrNullRefCore(TKey key) => ref Unsafe.NullRef(); + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/EmptyFrozenSet.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/EmptyFrozenSet.cs new file mode 100644 index 0000000000000..900e2d391eb7f --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/EmptyFrozenSet.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace System.Collections.Frozen +{ + /// Provides an empty to use when there are zero values to be stored. + internal sealed class EmptyFrozenSet : FrozenSet + { + internal EmptyFrozenSet() : base(EqualityComparer.Default) { } + + /// + private protected override ImmutableArray ItemsCore => ImmutableArray.Empty; + + /// + private protected override int CountCore => 0; + + /// + private protected override int FindItemIndex(T item) => -1; + + /// + private protected override Enumerator GetEnumeratorCore() => new Enumerator(Array.Empty()); + + /// + private protected override bool IsProperSubsetOfCore(IEnumerable other) => !OtherIsEmpty(other); + + /// + private protected override bool IsProperSupersetOfCore(IEnumerable other) => false; + + /// + private protected override bool IsSubsetOfCore(IEnumerable other) => true; + + /// + private protected override bool IsSupersetOfCore(IEnumerable other) => OtherIsEmpty(other); + + /// + private protected override bool OverlapsCore(IEnumerable other) => false; + + /// + private protected override bool SetEqualsCore(IEnumerable other) => OtherIsEmpty(other); + + private static bool OtherIsEmpty(IEnumerable other) => + other is IReadOnlyCollection s ? s.Count == 0 : // TODO https://github.com/dotnet/runtime/issues/42254: Remove if/when Any includes this check + !other.Any(); + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Freezer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Freezer.cs deleted file mode 100644 index c54eaa8e639e2..0000000000000 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Freezer.cs +++ /dev/null @@ -1,190 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; - -namespace System.Collections.Immutable -{ - /// - /// Factory methods for frozen collections. - /// - /// - /// Frozen collections are immutable and are optimized for situations where a collection - /// is created infrequently, but used repeatedly at runtime. They have a relatively high - /// cost to create, but provide excellent lookup performance. These are thus ideal for cases - /// where a collection is created at startup of an application and used throughout the life - /// of the application. - /// - public static class Freezer - { - /// - /// Initializes a new dictionary with the given set of key/value pairs. - /// - /// The pairs to initialize the dictionary with. - /// The comparer used to compare and hash keys. If this is null, then is used. - /// The type of the keys in the dictionary. - /// The type of the values in the dictionary. - /// If more than 64K pairs are added. - /// - /// Tf the same key appears multiple times in the input, the latter one in the sequence takes precedence. - /// - /// A frozen dictionary. - public static FrozenDictionary ToFrozenDictionary(this IEnumerable>? pairs, IEqualityComparer comparer) - where TKey : notnull - { - return new FrozenDictionary(pairs.EmptyIfNull(), comparer); - } - - /// - /// Initializes a new dictionary with the given set of key/value pairs. - /// - /// The pairs to initialize the dictionary with. - /// The type of the keys in the dictionary. - /// The type of the values in the dictionary. - /// If more than 64K pairs are added. - /// - /// Tf the same key appears multiple times in the input, the latter one in the sequence takes precedence. - /// - /// If your dictionary's keys are strings compared using ordinal string comparison, you should be using the - /// overload instead - /// as it will give you a specialized dictionary which delivers higher performance for ordinal string keys. - /// - /// If your dictionary's keys are integers, you should be using the - /// overload instead, also for higher performance. - /// - /// A frozen dictionary. - public static FrozenDictionary ToFrozenDictionary(this IEnumerable>? pairs) - where TKey : notnull - { - return new FrozenDictionary(pairs.EmptyIfNull(), EqualityComparer.Default); - } - - /// - /// Initializes a new dictionary with the given set of key/value pairs. - /// - /// The pairs to initialize the dictionary with. - /// The type of the values in the dictionary. - /// If more than 64K pairs are added. - /// - /// Tf the same key appears multiple times in the input, the latter one in the sequence takes precedence. - /// - /// A frozen dictionary. - public static FrozenIntDictionary ToFrozenDictionary(this IEnumerable>? pairs) - { - return new FrozenIntDictionary(pairs.EmptyIfNull()); - } - - /// - /// Initializes a new dictionary with the given set of key/value pairs. - /// - /// The pairs to initialize the dictionary with. - /// Whether to use case-insensitive semantics. - /// The type of the values in the dictionary. - /// If more than 64K pairs are added. - /// - /// Tf the same key appears multiple times in the input, the latter one in the sequence takes precedence. - /// - /// A frozen dictionary. - public static FrozenOrdinalStringDictionary ToFrozenDictionary(this IEnumerable>? pairs, bool ignoreCase) - { - return new FrozenOrdinalStringDictionary(pairs.EmptyIfNull(), ignoreCase); - } - - /// - /// Initializes a new dictionary with the given set of key/value pairs using case-sensitive ordinal string comparison. - /// - /// The pairs to initialize the dictionary with. - /// The type of the values in the dictionary. - /// If more than 64K pairs are added. - /// - /// Tf the same key appears multiple times in the input, the latter one in the sequence takes precedence. - /// - /// A frozen dictionary. - public static FrozenOrdinalStringDictionary ToFrozenDictionary(this IEnumerable>? pairs) - { - return new FrozenOrdinalStringDictionary(pairs.EmptyIfNull(), false); - } - - /// - /// Initializes a new set with the given items. - /// - /// The items to initialize the set with. - /// The comparer used to compare and hash items. - /// The type of the items in the set. - /// If more than 64K items are added. - /// A frozen set. - public static FrozenSet ToFrozenSet(this IEnumerable? items, IEqualityComparer comparer) - where T : notnull - { - return new FrozenSet(items.EmptyIfNull(), comparer); - } - - /// - /// Initializes a new set with the given items. - /// - /// The items to initialize the set with. - /// The type of the items in the set. - /// If more than 64K items are added. - /// - /// If your set's items are strings compared using ordinal string comparison, you should be using the - /// overload instead - /// as it will give you a specialized set which delivers higher performance for ordinal string items. - /// - /// If your set's items are integers, you should be using the - /// overload instead, also for higher performance. - /// - /// A frozen set. - public static FrozenSet ToFrozenSet(this IEnumerable? items) - where T : notnull - { - return new FrozenSet(items.EmptyIfNull(), EqualityComparer.Default); - } - - /// - /// Initializes a new set with the given items. - /// - /// The items to initialize the set with. - /// If more than 64K items are added. - /// A frozen set. - public static FrozenIntSet ToFrozenSet(this IEnumerable? items) - { - return new FrozenIntSet(items.EmptyIfNull()); - } - - /// - /// Initializes a new set with the given items. - /// - /// The items to initialize the set with. - /// Whether to use case-insensitive semantics. - /// If more than 64K items are added. - /// A frozen set. - public static FrozenOrdinalStringSet ToFrozenSet(this IEnumerable? items, bool ignoreCase) - { - return new FrozenOrdinalStringSet(items.EmptyIfNull(), ignoreCase); - } - - /// - /// Initializes a new set with the given items. - /// - /// The items to initialize the set with. - /// If more than 64K items are added. - /// A frozen set. - public static FrozenOrdinalStringSet ToFrozenSet(this IEnumerable? items) - { - return new FrozenOrdinalStringSet(items.EmptyIfNull()); - } - - /// - /// Initializes a new list with the given items. - /// - /// The items to initialize the list with. - /// The type of elements in the list. - /// A frozen list. - public static FrozenList ToFrozenList(this IEnumerable? items) - { - return new FrozenList(items.EmptyIfNull()); - } - - private static IEnumerable EmptyIfNull(this IEnumerable? items) => items ?? Array.Empty(); - } -} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenDictionary.cs index a2ac33d341c2d..2ea2d8c8b4caf 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenDictionary.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenDictionary.cs @@ -1,346 +1,515 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.ComponentModel; +using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Runtime.CompilerServices; -namespace System.Collections.Immutable +namespace System.Collections.Frozen { /// - /// A frozen dictionary. + /// Provides a set of initialization methods for instances of the class. /// - /// The type of the keys in the dictionary. - /// The type of the values in the dictionary. /// - /// Frozen dictionaries are immutable and are optimized for situations where a dictionary - /// is created infrequently, but used repeatedly at runtime. They have a relatively high - /// cost to create, but provide excellent lookup performance. These are thus ideal for cases - /// where a dictionary is created at startup of an application and used throughout the life - /// of the application. - /// - /// This is the general-purpose frozen dictionary which can be used with any key type. If you need - /// a dictionary that has a string or integer as key, you will get better performance by using - /// or - /// respectively. + /// Frozen collections are immutable and are optimized for situations where a collection + /// is created very infrequently but is used very frequently at runtime. They have a relatively high + /// cost to create but provide excellent lookup performance. Thus, these are ideal for cases + /// where a collection is created once, potentially at the startup of an application, and used throughout + /// the remainder of the life of the application. Frozen collections should only be initialized with + /// trusted input. /// - [DebuggerTypeProxy(typeof(IFrozenDictionaryDebugView<,>))] - [DebuggerDisplay("Count = {Count}")] - public readonly struct FrozenDictionary : IFrozenDictionary, IDictionary - where TKey : notnull + public static class FrozenDictionary { - private readonly FrozenHashTable _hashTable; - private readonly TKey[] _keys; - private readonly TValue[] _values; - - /// - /// Gets an empty frozen dictionary. - /// - public static FrozenDictionary Empty => new(Array.Empty>(), EqualityComparer.Default); - - /// - /// Initializes a new instance of the struct. - /// - /// The pairs to initialize the dictionary with. - /// The comparer used to compare and hash keys. - /// If more than 64K pairs are added. + /// Creates a with the specified key/value pairs. + /// The key/value pairs to use to populate the dictionary. + /// The comparer implementation to use to compare keys for equality. If null, is used. + /// The type of the keys in the dictionary. + /// The type of the values in the dictionary. /// - /// Tf the same key appears multiple times in the input, the latter one in the sequence takes precedence. + /// If the same key appears multiple times in the input, the latter one in the sequence takes precedence. This differs from , + /// with which multiple duplicate keys will result in an exception. /// - internal FrozenDictionary(IEnumerable> pairs, IEqualityComparer comparer) + /// A that contains the specified keys and values. + public static FrozenDictionary ToFrozenDictionary(this IEnumerable> source, IEqualityComparer? comparer = null) + where TKey : notnull { - KeyValuePair[] incoming = MakeUniqueArray(pairs, comparer); + ThrowHelper.ThrowIfNull(source); + comparer ??= EqualityComparer.Default; - if (ReferenceEquals(comparer, StringComparer.Ordinal) || - ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase)) + // If the source is already frozen with the same comparer, it can simply be returned. + if (source is FrozenDictionary existing && + existing.Comparer.Equals(comparer)) { - comparer = (IEqualityComparer)ComparerPicker.Pick(SetSupport.ExtractStringKeysToArray(incoming), ignoreCase: ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase)); + return existing; } - _keys = incoming.Length == 0 ? Array.Empty() : new TKey[incoming.Length]; - _values = incoming.Length == 0 ? Array.Empty() : new TValue[incoming.Length]; - Comparer = comparer; - - TKey[] keys = _keys; - TValue[] values = _values; - _hashTable = FrozenHashTable.Create( - incoming, - pair => comparer.GetHashCode(pair.Key), - (index, pair) => + // Ensure we have a Dictionary<,> using the specified comparer such that all keys + // are non-null and unique according to that comparer. + if (source is not Dictionary uniqueValues || + (uniqueValues.Count != 0 && !uniqueValues.Comparer.Equals(comparer))) + { + uniqueValues = new Dictionary(comparer); + foreach (KeyValuePair pair in source) { - keys[index] = pair.Key; - values[index] = pair.Value; - }); + uniqueValues[pair.Key] = pair.Value; + } + } + + return Freeze(uniqueValues); } - private static KeyValuePair[] MakeUniqueArray(IEnumerable> pairs, IEqualityComparer comp) + /// Creates a from an according to specified key selector function. + /// The type of the elements of . + /// The type of the key returned by . + /// An from which to create a . + /// A function to extract a key from each element. + /// An to compare keys. + /// A that contains the keys and values selected from the input sequence. + public static FrozenDictionary ToFrozenDictionary( + this IEnumerable source, Func keySelector, IEqualityComparer? comparer = null) + where TKey : notnull => + Freeze(source.ToDictionary(keySelector, comparer)); + + /// Creates a from an according to specified key selector and element selector functions. + /// The type of the elements of . + /// The type of the key returned by . + /// The type of the value returned by . + /// An from which to create a . + /// A function to extract a key from each element. + /// A transform function to produce a result element value from each element. + /// An to compare keys. + /// A that contains the keys and values selected from the input sequence. + public static FrozenDictionary ToFrozenDictionary( + this IEnumerable source, Func keySelector, Func elementSelector, IEqualityComparer? comparer = null) + where TKey : notnull => + Freeze(source.ToDictionary(keySelector, elementSelector, comparer)); + + private static FrozenDictionary Freeze(Dictionary source) + where TKey : notnull { - if (!(pairs is Dictionary dict && dict.Comparer.Equals(comp))) + // If the input was empty, simply return the empty frozen dictionary singleton. The comparer is ignored. + if (source.Count == 0) { - dict = new Dictionary(comp); - foreach (KeyValuePair pair in pairs) - { - dict[pair.Key] = pair.Value; - } + return FrozenDictionary.Empty; } - if (dict.Count == 0) + IEqualityComparer comparer = source.Comparer; + + if (typeof(TKey).IsValueType) { - return Array.Empty>(); + // Optimize for value types when the default comparer is being used. In such a case, the implementation + // may use EqualityComparer.Default.Equals/GetHashCode directly, with generic specialization enabling + // the Equals/GetHashCode methods to be devirtualized and possibly inlined. + if (ReferenceEquals(comparer, EqualityComparer.Default)) + { + // In the specific case of Int32 keys, we can optimize further to reduce memory consumption by using + // the underlying FrozenHashtable's Int32 index as the keys themselves, avoiding the need to store the + // same keys yet again. + return typeof(TKey) == typeof(int) ? + (FrozenDictionary)(object)new Int32FrozenDictionary((Dictionary)(object)source) : + new ValueTypeDefaultComparerFrozenDictionary(source); + } } + else if (typeof(TKey) == typeof(string)) + { + // If the key is a string and the comparer is known to provide ordinal (case-sensitive or case-insensitive) semantics, + // we can use an implementation that's able to examine and optimize based on lengths and/or subsequences within those strings. + if (ReferenceEquals(comparer, EqualityComparer.Default) || + ReferenceEquals(comparer, StringComparer.Ordinal) || + ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase)) + { + Dictionary stringEntries = (Dictionary)(object)source; + IEqualityComparer stringComparer = (IEqualityComparer)(object)comparer; - var result = new KeyValuePair[dict.Count]; - ((ICollection>)dict).CopyTo(result, 0); + FrozenDictionary frozenDictionary = + LengthBucketsFrozenDictionary.TryCreateLengthBucketsFrozenSet(stringEntries, stringComparer) ?? + (FrozenDictionary)new OrdinalStringFrozenDictionary(stringEntries, stringComparer); - return result; + return (FrozenDictionary)(object)frozenDictionary; + } + } + + // No special-cases apply. Use the default frozen dictionary. + return new DefaultFrozenDictionary(source, comparer); } + } + + /// Provides an immutable, read-only dictionary optimized for fast lookup and enumeration. + /// The type of the keys in the dictionary. + /// The type of the values in this dictionary. + /// + /// Frozen collections are immutable and are optimized for situations where a collection + /// is created very infrequently but is used very frequently at runtime. They have a relatively high + /// cost to create but provide excellent lookup performance. Thus, these are ideal for cases + /// where a collection is created once, potentially at the startup of an application, and used throughout + /// the remainder of the life of the application. Frozen collections should only be initialized with + /// trusted input. + /// + [DebuggerTypeProxy(typeof(ImmutableDictionaryDebuggerProxy<,>))] + [DebuggerDisplay("Count = {Count}")] + public abstract class FrozenDictionary : IDictionary, IReadOnlyDictionary, IDictionary + where TKey : notnull + { + /// Initialize the dictionary. + /// The comparer to use and to expose from . + private protected FrozenDictionary(IEqualityComparer comparer) => Comparer = comparer; + + /// Gets an empty . + public static FrozenDictionary Empty { get; } = new EmptyFrozenDictionary(); + + /// Gets the comparer used by this dictionary. + public IEqualityComparer Comparer { get; } + + /// + /// Gets a collection containing the keys in the dictionary. + /// + /// + /// The order of the keys in the dictionary is unspecified, but it is the same order as the associated values returned by the property. + /// + public ImmutableArray Keys => KeysCore; + + /// + private protected abstract ImmutableArray KeysCore { get; } /// - public FrozenList Keys => new(_keys); + ICollection IDictionary.Keys => + Keys is { Length: > 0 } keys ? keys : Array.Empty(); /// - public FrozenList Values => new(_values); + IEnumerable IReadOnlyDictionary.Keys => + ((IDictionary)this).Keys; /// - public FrozenPairEnumerator GetEnumerator() => new(_keys, _values); + ICollection IDictionary.Keys => Keys; /// - /// Gets an enumeration of the dictionary's keys. + /// Gets a collection containing the values in the dictionary. /// - IEnumerable IReadOnlyDictionary.Keys => Count > 0 ? _keys : Array.Empty(); + /// + /// The order of the values in the dictionary is unspecified, but it is the same order as the associated keys returned by the property. + /// + public ImmutableArray Values => ValuesCore; - /// - /// Gets an enumeration of the dictionary's values. - /// - IEnumerable IReadOnlyDictionary.Values => Count > 0 ? _values : Array.Empty(); + /// + private protected abstract ImmutableArray ValuesCore { get; } - /// - /// Gets an enumeration of the dictionary's key/value pairs. - /// - /// The enumerator. - IEnumerator> IEnumerable>.GetEnumerator() - => Count > 0 ? GetEnumerator() : ((IList>)Array.Empty>()).GetEnumerator(); + ICollection IDictionary.Values => + Values is { Length: > 0 } values ? values : Array.Empty(); - /// - /// Gets an enumeration of the dictionary's key/value pairs. - /// - /// The enumerator. - IEnumerator IEnumerable.GetEnumerator() => Count > 0 ? GetEnumerator() : ((IList>)Array.Empty>()).GetEnumerator(); + /// + ICollection IDictionary.Values => Values; - /// - /// Gets the number of key/value pairs in the dictionary. - /// - public int Count => _hashTable.Count; + /// + IEnumerable IReadOnlyDictionary.Values => + ((IDictionary)this).Values; - /// - /// Gets the comparer used by this dictionary. - /// - public IEqualityComparer Comparer { get; } + /// Gets the number of key/value pairs contained in the dictionary. + public int Count => CountCore; - /// - /// Gets the value associated to the given key. - /// - /// The key to lookup. - /// The associated value. - /// If the key doesn't exist in the dictionary. - public TValue this[TKey key] + /// + private protected abstract int CountCore { get; } + + /// Copies the elements of the dictionary to an array of type , starting at the specified . + /// The array that is the destination of the elements copied from the dictionary. + /// The zero-based index in at which copying begins. + public void CopyTo(KeyValuePair[] destination, int destinationIndex) { - get - { - int hashCode = Comparer.GetHashCode(key); - _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); + ThrowHelper.ThrowIfNull(destination); + CopyTo(destination.AsSpan(destinationIndex)); + } - while (index <= endIndex) - { - if (hashCode == _hashTable.EntryHashCode(index)) - { - if (Comparer.Equals(key, _keys[index])) - { - return _values[index]; - } - } + /// Copies the elements of the dictionary to a span of type . + /// The span that is the destination of the elements copied from the dictionary. + public void CopyTo(Span> destination) + { + if (destination.Length < Count) + { + ThrowHelper.ThrowIfDestinationTooSmall(); + } - index++; - } + TKey[] keys = Keys.array!; + TValue[] values = Values.array!; + Debug.Assert(keys.Length == values.Length); - throw new KeyNotFoundException(); + for (int i = 0; i < keys.Length; i++) + { + destination[i] = new KeyValuePair(keys[i], values[i]); } } - /// - /// Checks whether a particular key exists in the dictionary. - /// - /// The key to probe for. - /// if the key is in the dictionary, otherwise . - public bool ContainsKey(TKey key) + /// + void ICollection.CopyTo(Array array, int index) { - int hashCode = Comparer.GetHashCode(key); - _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); + ThrowHelper.ThrowIfNull(array); - while (index <= endIndex) + if (array.Rank != 1) { - if (hashCode == _hashTable.EntryHashCode(index)) - { - if (Comparer.Equals(key, _keys[index])) - { - return true; - } - } + throw new ArgumentException(SR.Arg_RankMultiDimNotSupported, nameof(array)); + } - index++; + if (array.GetLowerBound(0) != 0) + { + throw new ArgumentException(SR.Arg_NonZeroLowerBound, nameof(array)); } - return false; - } + if ((uint)index > (uint)array.Length) + { + throw new ArgumentOutOfRangeException(nameof(index), SR.ArgumentOutOfRange_NeedNonNegNum); + } - /// - /// Tries to get a value associated with a specific key. - /// - /// The key to lookup. - /// The value associated with the key. - /// if the key was found, otherwise . - public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) - { - int hashCode = Comparer.GetHashCode(key); - _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); + if (array.Length - index < Count) + { + throw new ArgumentException(SR.Arg_ArrayPlusOffTooSmall, nameof(array)); + } - while (index <= endIndex) + if (array is KeyValuePair[] pairs) { - if (hashCode == _hashTable.EntryHashCode(index)) + foreach (KeyValuePair item in this) { - if (Comparer.Equals(key, _keys[index])) + pairs[index++] = new KeyValuePair(item.Key, item.Value); + } + } + else if (array is DictionaryEntry[] dictEntryArray) + { + foreach (KeyValuePair item in this) + { + dictEntryArray[index++] = new DictionaryEntry(item.Key, item.Value); + } + } + else + { + if (array is not object[] objects) + { + throw new ArgumentException(SR.Argument_InvalidArrayType, nameof(array)); + } + + try + { + foreach (KeyValuePair item in this) { - value = _values[index]; - return true; + objects[index++] = new KeyValuePair(item.Key, item.Value); } } - - index++; + catch (ArrayTypeMismatchException) + { + throw new ArgumentException(SR.Argument_InvalidArrayType, nameof(array)); + } } - - value = default!; - return false; } /// - public ref readonly TValue GetByRef(TKey key) - { - int hashCode = Comparer.GetHashCode(key); - _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); + bool ICollection>.IsReadOnly => true; - while (index <= endIndex) + /// + bool IDictionary.IsReadOnly => true; + + /// + bool IDictionary.IsFixedSize => true; + + /// + bool ICollection.IsSynchronized => false; + + /// + object ICollection.SyncRoot => this; + + /// + object? IDictionary.this[object key] + { + get { - if (hashCode == _hashTable.EntryHashCode(index)) - { - if (Comparer.Equals(key, _keys[index])) - { - return ref _values[index]; - } - } + ThrowHelper.ThrowIfNull(key); + return key is TKey tkey && TryGetValue(tkey, out TValue? value) ? + value : + (object?)null; + } + set => throw new NotSupportedException(); + } - index++; + /// Gets either a reference to a in the dictionary or a null reference if the key does not exist in the dictionary. + /// The key used for lookup. + /// A reference to a in the dictionary or a null reference if the key does not exist in the dictionary. + /// The null reference can be detected by calling . + public ref readonly TValue GetValueRefOrNullRef(TKey key) + { + if (key is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(key)); } - throw new KeyNotFoundException(); + return ref GetValueRefOrNullRefCore(key); } - /// - public ref readonly TValue TryGetByRef(TKey key) - { - int hashCode = Comparer.GetHashCode(key); - _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); + /// + private protected abstract ref readonly TValue GetValueRefOrNullRefCore(TKey key); - while (index <= endIndex) + /// Gets a reference to the value associated with the specified key. + /// The key of the value to get. + /// A reference to the value associated with the specified key. + /// does not exist in the collection. + public ref readonly TValue this[TKey key] + { + get { - if (hashCode == _hashTable.EntryHashCode(index)) + ref readonly TValue valueRef = ref GetValueRefOrNullRef(key); + + if (Unsafe.IsNullRef(ref Unsafe.AsRef(in valueRef))) { - if (Comparer.Equals(key, _keys[index])) - { - return ref _values[index]; - } + ThrowHelper.ThrowKeyNotFoundException(); } - index++; + return ref valueRef; } + } - return ref Unsafe.NullRef(); + /// + TValue IDictionary.this[TKey key] + { + get => this[key]; + set => throw new NotSupportedException(); } - /// - /// Copies the content of the dictionary to a span. - /// - /// The destination where to copy to. - public void CopyTo(Span> destination) + /// + TValue IReadOnlyDictionary.this[TKey key] => + this[key]; + + /// Determines whether the dictionary contains the specified key. + /// The key to locate in the dictionary. + /// if the dictionary contains an element with the specified key; otherwise, . + public bool ContainsKey(TKey key) => + !Unsafe.IsNullRef(ref Unsafe.AsRef(in GetValueRefOrNullRef(key))); + + /// + bool IDictionary.Contains(object key) { - ThrowHelper.IfBufferTooSmall(destination.Length, Count); + ThrowHelper.ThrowIfNull(key); + return key is TKey tkey && ContainsKey(tkey); + } - for (int i = 0; i < Count; i++) + /// + bool ICollection>.Contains(KeyValuePair item) => + TryGetValue(item.Key, out TValue? value) && + EqualityComparer.Default.Equals(value, item.Value); + + /// Gets the value associated with the specified key. + /// The key of the value to get. + /// + /// When this method returns, contains the value associated with the specified key, if the key is found; + /// otherwise, the default value for the type of the value parameter. + /// + /// if the dictionary contains an element with the specified key; otherwise, . + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + ref readonly TValue valueRef = ref GetValueRefOrNullRef(key); + + if (!Unsafe.IsNullRef(ref Unsafe.AsRef(in valueRef))) { - destination[i] = new KeyValuePair(_keys[i], _values[i]); + value = valueRef; + return true; } + + value = default; + return false; } - /// - /// Copies the content of the dictionary to an array. - /// - /// The destination where to copy to. - /// Index into the array where to start copying the data. - public void CopyTo(KeyValuePair[] array, int arrayIndex) => CopyTo(array.AsSpan(arrayIndex)); + /// Returns an enumerator that iterates through the dictionary. + /// An enumerator that iterates through the dictionary. + public Enumerator GetEnumerator() => GetEnumeratorCore(); - /// - /// Gets a value indicating whether this collection is a read-only collection. - /// - /// Always returns true. - bool ICollection>.IsReadOnly => true; + /// + private protected abstract Enumerator GetEnumeratorCore(); - /// - /// Gets a collection holding this dictionary's keys. - /// - ICollection IDictionary.Keys => Count > 0 ? _keys : Array.Empty(); + /// + IEnumerator> IEnumerable>.GetEnumerator() => + Count == 0 ? ((IList>)Array.Empty>()).GetEnumerator() : + GetEnumerator(); - /// - /// Gets a collection holding this dictionary's values. - /// - ICollection IDictionary.Values => Count > 0 ? _values : Array.Empty(); + /// + IEnumerator IEnumerable.GetEnumerator() => + Count == 0 ? Array.Empty>().GetEnumerator() : + GetEnumerator(); - /// - /// Determines whether the dictionary contains the given key/value pair. - /// - /// The item to search for. - /// if the item is in the dictionary, otherwise . - bool ICollection>.Contains(KeyValuePair item) + /// + IDictionaryEnumerator IDictionary.GetEnumerator() => + new DictionaryEnumerator(GetEnumerator()); + + /// + void IDictionary.Add(TKey key, TValue value) => throw new NotSupportedException(); + + /// + void ICollection>.Add(KeyValuePair item) => throw new NotSupportedException(); + + /// + void IDictionary.Add(object key, object? value) => throw new NotSupportedException(); + + /// + bool IDictionary.Remove(TKey key) => throw new NotSupportedException(); + + /// + bool ICollection>.Remove(KeyValuePair item) => throw new NotSupportedException(); + + /// + void IDictionary.Remove(object key) => throw new NotSupportedException(); + + /// + void ICollection>.Clear() => throw new NotSupportedException(); + + /// + void IDictionary.Clear() => throw new NotSupportedException(); + + /// Enumerates the elements of a . + public struct Enumerator : IEnumerator> { - ref readonly TValue v = ref TryGetByRef(item.Key); - if (Unsafe.IsNullRef(ref Unsafe.AsRef(in v))) + private readonly TKey[] _keys; + private readonly TValue[] _values; + private int _index; + + /// Initialize the enumerator with the specified keys and values. + internal Enumerator(TKey[] keys, TValue[] values) { - return false; + Debug.Assert(keys.Length == values.Length); + _keys = keys; + _values = values; + _index = -1; } - return EqualityComparer.Default.Equals(v, item.Value); - } + /// + public bool MoveNext() + { + _index++; + if ((uint)_index < (uint)_keys.Length) + { + return true; + } - [EditorBrowsable(EditorBrowsableState.Never)] - TValue IDictionary.this[TKey key] - { - get => this[key]; - set => throw new NotSupportedException(); - } + _index = _keys.Length; + return false; + } - [EditorBrowsable(EditorBrowsableState.Never)] - void ICollection>.Add(KeyValuePair item) => throw new NotSupportedException(); + /// + public readonly KeyValuePair Current + { + get + { + if ((uint)_index >= (uint)_keys.Length) + { + ThrowHelper.ThrowInvalidOperationException(); + } - [EditorBrowsable(EditorBrowsableState.Never)] - void ICollection>.Clear() => throw new NotSupportedException(); + return new KeyValuePair(_keys[_index], _values[_index]); + } + } - [EditorBrowsable(EditorBrowsableState.Never)] - bool ICollection>.Remove(KeyValuePair item) => throw new NotSupportedException(); + /// + object IEnumerator.Current => Current; - [EditorBrowsable(EditorBrowsableState.Never)] - void IDictionary.Add(TKey key, TValue value) => throw new NotSupportedException(); + /// + void IEnumerator.Reset() => _index = -1; - [EditorBrowsable(EditorBrowsableState.Never)] - bool IDictionary.Remove(TKey key) => throw new NotSupportedException(); + /// + void IDisposable.Dispose() { } + } } } diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenEnumerator.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenEnumerator.cs deleted file mode 100644 index b151aa412036d..0000000000000 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenEnumerator.cs +++ /dev/null @@ -1,78 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; - -namespace System.Collections.Immutable -{ - /// - /// Enumerates the entries of a frozen collection. - /// - /// The types of the collection's entries. - public struct FrozenEnumerator : IEnumerator - { - private readonly T[] _entries; - private int _index; - - internal FrozenEnumerator(T[] entries) - { - _entries = entries; - _index = -1; - } - - /// - /// Gets the current value held by the enumerator. - /// - public readonly T Current - { - get - { - if (_index >= 0) - { - return _entries[_index]; - } - - return Throw(); - } - } - - /// - /// Dispose this object. - /// - void IDisposable.Dispose() - { - // nothing to do - } - - /// - /// Advances the enumerator to the next item in the collection. - /// - /// if the enumerator was successfully advanced to the next item; if the enumerator has passed the end of the collection. - public bool MoveNext() - { - if (_index < _entries.Length - 1) - { - _index++; - return true; - } - - return false; - } - - /// - /// Resets the enumerator to its initial state. - /// - void IEnumerator.Reset() => _index = -1; - - /// - /// Gets the current value held by the enumerator. - /// - object IEnumerator.Current => Current!; - - // keep this separate to allow inlining of the Current property - private static T Throw() - { - throw new InvalidOperationException("Call MoveNext() before reading the Current property."); - } - } -} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenHashTable.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenHashTable.cs index d03c861d919ae..8ae7d526e1860 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenHashTable.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenHashTable.cs @@ -1,108 +1,86 @@ // 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; -namespace System.Collections.Immutable +namespace System.Collections.Frozen { - /// - /// A hash table for frozen collections. - /// + /// Provides the core hash table for use in frozen collections. /// - /// Frozen collections are immutable and are optimized for situations where a collection - /// is created infrequently, but used repeatedly at runtime. They have a relatively high - /// cost to create, but provide excellent lookup performance. These are thus ideal for cases - /// where the collection is created at startup of an application and used throughout the life - /// of the application. - /// - /// This hash table doesn't track any of the collection state. It merely keeps track of hash codes - /// and of mapping these hash codes to spans of entries within the collection. + /// This hash table doesn't track any of the collection state. It merely keeps track + /// of hash codes and of mapping these hash codes to spans of entries within the collection. /// internal readonly struct FrozenHashTable { - private static readonly Bucket[] _emptyBuckets = GetEmptyBuckets(); - private readonly Bucket[] _buckets; private readonly ulong _fastModMultiplier; private FrozenHashTable(int[] hashCodes, Bucket[] buckets, ulong fastModMultiplier) { + Debug.Assert(hashCodes.Length != 0); + Debug.Assert(buckets.Length != 0); + HashCodes = hashCodes; _buckets = buckets; _fastModMultiplier = fastModMultiplier; } - /// - /// Initializes a frozen hash table. - /// + /// Initializes a frozen hash table. /// The set of entries to track from the hash table. /// A delegate that produces a hash code for a given entry. /// A delegate that assigns the index to a specific entry. /// The type of elements in the hash table. - /// If more than 64K pairs are added. /// /// This method will iterate through the incoming entries and will invoke the hasher on each once. /// It will then determine the optimal number of hash buckets to allocate and will populate the /// bucket table. In the process of doing so, it calls out to the to indicate - /// the resulting index for that entry. The and - /// then use this index to reference individual entries. + /// the resulting index for that entry. + /// then uses this index to reference individual entries by indexing into . /// /// A frozen hash table. public static FrozenHashTable Create(T[] entries, Func hasher, Action setter) { - int numEntries = entries.Length; - - int[] hashCodes; - Bucket[] buckets; - ulong fastModMultiplier; + Debug.Assert(entries.Length != 0); - if (numEntries == 0) + int[] hashCodes = new int[entries.Length]; + for (int i = 0; i < entries.Length; i++) { - hashCodes = Array.Empty(); - buckets = _emptyBuckets; - fastModMultiplier = GetFastModMultiplier(1); + hashCodes[i] = hasher(entries[i]); } - else - { - hashCodes = new int[numEntries]; - for (int i = 0; i < numEntries; i++) - { - hashCodes[i] = hasher(entries[i]); - } - int numBuckets = CalcNumBuckets(hashCodes); - fastModMultiplier = GetFastModMultiplier((uint)numBuckets); + int numBuckets = CalcNumBuckets(hashCodes); + ulong fastModMultiplier = HashHelpers.GetFastModMultiplier((uint)numBuckets); + + var chainBuddies = new Dictionary>(); + for (int index = 0; index < hashCodes.Length; index++) + { + int hashCode = hashCodes[index]; + uint bucket = HashHelpers.FastMod((uint)hashCode, (uint)numBuckets, fastModMultiplier); - var chainBuddies = new Dictionary>(); - for (int index = 0; index < numEntries; index++) + if (!chainBuddies.TryGetValue(bucket, out List? list)) { - int hashCode = hashCodes[index]; - uint bucket = FastMod((uint)hashCode, (uint)numBuckets, fastModMultiplier); + chainBuddies[bucket] = list = new List(); + } - if (!chainBuddies.TryGetValue(bucket, out List? list)) - { - list = new List(); - chainBuddies[bucket] = list; - } + list.Add(new ChainBuddy(hashCode, index)); + } - list.Add(new ChainBuddy(hashCode, index)); - } + var buckets = new Bucket[numBuckets]; - buckets = new Bucket[numBuckets]; + int count = 0; + foreach (List list in chainBuddies.Values) + { + uint bucket = HashHelpers.FastMod((uint)list[0].HashCode, (uint)buckets.Length, fastModMultiplier); - int count = 0; - foreach (List list in chainBuddies.Values) + buckets[bucket] = new Bucket(count, list.Count); + for (int i = 0; i < list.Count; i++) { - uint bucket = FastMod((uint)list[0].HashCode, (uint)buckets.Length, fastModMultiplier); - - buckets[bucket] = new Bucket(count, list.Count); - for (int i = 0; i < list.Count; i++) - { - hashCodes[count] = list[i].HashCode; - setter(count, entries[list[i].Index]); - count++; - } + hashCodes[count] = list[i].HashCode; + setter(count, entries[list[i].Index]); + count++; } } @@ -118,92 +96,108 @@ public static FrozenHashTable Create(T[] entries, Func hasher, Action [MethodImpl(MethodImplOptions.AggressiveInlining)] public void FindMatchingEntries(int hashCode, out int startIndex, out int endIndex) { - ref Bucket b = ref _buckets[FastMod((uint)hashCode, (uint)_buckets.Length, _fastModMultiplier)]; + ref Bucket b = ref _buckets[HashHelpers.FastMod((uint)hashCode, (uint)_buckets.Length, _fastModMultiplier)]; startIndex = b.StartIndex; endIndex = b.EndIndex; } public int Count => HashCodes.Length; - /// - /// Given an entry, get its hash code. - /// - /// The entry index to probe. - /// The hash code belonging to this entry. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int EntryHashCode(int entry) => HashCodes[entry]; - internal int[] HashCodes { get; } - private static Bucket[] GetEmptyBuckets() - { - var buckets = new Bucket[1]; - buckets[0] = new Bucket(1, 0); - return buckets; - } - /// /// Given an array of hash codes, figure out the best number of hash buckets to use. /// /// - /// At the moment, this is pretty dumb. It sits in a loop trying a large number of hash buckets - /// and seeing how many collisions there are. It stops when there are less than 5% collisions. - /// For a large table with a lot of collisions, this could take an awful long time. Is there a - /// better strategy possible here? For example, perhaps we should first try to scale the bucket count - /// to prime values. + /// This tries to select a prime number of buckets. Rather than iterating through all possible bucket + /// sizes, starting at the exact number of hash codes and incrementing the bucket count by 1 per trial, + /// this is a trade-off between speed of determining a good number of buckets and maximumal density. /// private static int CalcNumBuckets(int[] hashCodes) { - // how big of a bucket table to allow for small inputs - const int MaxSmallBucketTableMultiplier = 16; - - // how big of a bucket table to allow for large inputs - const int MaxLargeBucketTableMultiplier = 3; - - // what is the limit for a small input? - const int LargeInputSize = 1000; + const double AcceptableCollisionRate = 0.05; // What is a satifactory rate of hash collisions? + const int LargeInputSizeThreshold = 1000; // What is the limit for an input to be considered "small"? + const int MaxSmallBucketTableMultiplier = 16; // How large a bucket table should be allowed for small inputs? + const int MaxLargeBucketTableMultiplier = 3; // How large a bucket table should be allowed for large inputs? + + // Filter out duplicate codes, since no increase in buckets will avoid collisions from duplicate input hash codes. + var codes = new HashSet(hashCodes); + Debug.Assert(codes.Count != 0); + + // In our precomputed primes table, find the index of the smallest prime that's at least as large as our number of + // hash codes. If there are more codes than in our precomputed primes table, which accomodates millions of values, + // give up and just use the next prime. + int minPrimeIndexInclusive = 0; + while (minPrimeIndexInclusive < HashHelpers.s_primes.Length && codes.Count > HashHelpers.s_primes[minPrimeIndexInclusive]) + { + minPrimeIndexInclusive++; + } + if (minPrimeIndexInclusive >= HashHelpers.s_primes.Length) + { + return HashHelpers.GetPrime(codes.Count); + } - // what is the satifactory rate of hash collisions? - const double AcceptableCollisionRate = 0.05; + // Determine the largest number of buckets we're willing to use, based on a multiple of the number of inputs. + // For smaller inputs, we allow for a larger multiplier. + int maxNumBuckets = + codes.Count * + (codes.Count >= LargeInputSizeThreshold ? MaxLargeBucketTableMultiplier : MaxSmallBucketTableMultiplier); - // filter out duplicate codes - var codesSet = new HashSet(hashCodes); - int[] codes = new int[codesSet.Count]; - codesSet.CopyTo(codes); + // Find the index of the smallest prime that accomodates our max buckets. + int maxPrimeIndexExclusive = minPrimeIndexInclusive; + while (maxPrimeIndexExclusive < HashHelpers.s_primes.Length && maxNumBuckets > HashHelpers.s_primes[maxPrimeIndexExclusive]) + { + maxPrimeIndexExclusive++; + } + if (maxPrimeIndexExclusive < HashHelpers.s_primes.Length) + { + Debug.Assert(maxPrimeIndexExclusive != 0); + maxNumBuckets = HashHelpers.s_primes[maxPrimeIndexExclusive - 1]; + } - int maxNumBuckets = (codes.Length >= LargeInputSize) ? codes.Length * MaxLargeBucketTableMultiplier : codes.Length * MaxSmallBucketTableMultiplier; - var buckets = new BitArray(maxNumBuckets); + const int BitsPerInt32 = 32; + int[] seenBuckets = ArrayPool.Shared.Rent((maxNumBuckets / BitsPerInt32) + 1); int bestNumBuckets = maxNumBuckets; - int bestNumCollisions = codes.Length; - for (int numBuckets = codes.Length; numBuckets < maxNumBuckets; numBuckets++) + int bestNumCollisions = codes.Count; + + // Iterate through each available prime between the min and max discovered. For each, compute + // the collision ratio. + for (int primeIndex = minPrimeIndexInclusive; primeIndex < maxPrimeIndexExclusive; primeIndex++) { - buckets.SetAll(false); + // Get the number of buckets to try, and clear our seen bucket bitmap. + int numBuckets = HashHelpers.s_primes[primeIndex]; + Array.Clear(seenBuckets, 0, Math.Min(numBuckets, seenBuckets.Length)); + // Determine the bucket for each hash code and mark it as seen. If it was already seen, + // track it as a collision. int numCollisions = 0; foreach (int code in codes) { uint bucketNum = (uint)code % (uint)numBuckets; - if (buckets[(int)bucketNum]) + if ((seenBuckets[bucketNum / BitsPerInt32] & (1 << (int)bucketNum)) != 0) { numCollisions++; if (numCollisions >= bestNumCollisions) { - // no sense in continuing, we already have more collisions than the best so far + // If we've already hit the previously known best number of collisions, + // there's no point in continuing as worst case we'd just use that. break; } } else { - buckets[(int)bucketNum] = true; + seenBuckets[bucketNum / BitsPerInt32] |= 1 << (int)bucketNum; } } + // If this evaluation resulted in fewer collisions, use it as the best instead. + // And if it's below our collision threshold, we're done. if (numCollisions < bestNumCollisions) { bestNumBuckets = numBuckets; - if (numCollisions / (double)codes.Length <= AcceptableCollisionRate) + if (numCollisions / (double)codes.Count <= AcceptableCollisionRate) { break; } @@ -212,20 +206,9 @@ private static int CalcNumBuckets(int[] hashCodes) } } - return bestNumBuckets; - } - - /// Returns approximate reciprocal of the divisor: ceil(2**64 / divisor). - private static ulong GetFastModMultiplier(uint divisor) => (ulong.MaxValue / divisor) + 1; + ArrayPool.Shared.Return(seenBuckets); - /// Performs a mod operation using the multiplier pre-computed with . - /// This should only be used on 64-bit architectures. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint FastMod(uint value, uint divisor, ulong multiplier) - { - // Modification of https://github.com/dotnet/runtime/pull/406, - // which allows to avoid the long multiplication if the divisor is less than 2**31. - return (uint)(((((multiplier * value) >> 32) + 1) * divisor) >> 32); + return bestNumBuckets; } private readonly struct ChainBuddy @@ -247,16 +230,10 @@ private readonly struct Bucket public Bucket(int index, int count) { - if (count == 0) - { - StartIndex = 1; - EndIndex = 0; - } - else - { - StartIndex = index; - EndIndex = index + count - 1; - } + Debug.Assert(count > 0); + + StartIndex = index; + EndIndex = index + count - 1; } } } diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenIntDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenIntDictionary.cs deleted file mode 100644 index 5d648ce160c65..0000000000000 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenIntDictionary.cs +++ /dev/null @@ -1,300 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - -namespace System.Collections.Immutable -{ - /// - /// A frozen dictionary with integer keys. - /// - /// The type of the values in the dictionary. - /// - /// Frozen dictionaries are immutable and are optimized for situations where a dictionary - /// is created infrequently, but used repeatedly at runtime. They have a relatively high - /// cost to create, but provide excellent lookup performance. These are thus ideal for cases - /// where a dictionary is created at startup of an application and used throughout the life - /// of the application. - /// - [DebuggerTypeProxy(typeof(IFrozenIntDictionaryDebugView<>))] - [DebuggerDisplay("Count = {Count}")] - public readonly struct FrozenIntDictionary : IFrozenDictionary, IDictionary - { - private readonly FrozenHashTable _hashTable; - private readonly TValue[] _values; - - /// - /// Gets an empty frozen integer dictionary. - /// - public static FrozenIntDictionary Empty => new(Array.Empty>()); - - /// - /// Initializes a new instance of the struct. - /// - /// The pairs to initialize the dictionary with. - /// If more than 64K pairs are added. - /// - /// Tf the same key appears multiple times in the input, the latter one in the sequence takes precedence. - /// - internal FrozenIntDictionary(IEnumerable> pairs) - { - KeyValuePair[] incoming = MakeUniqueArray(pairs); - - _values = incoming.Length == 0 ? Array.Empty() : new TValue[incoming.Length]; - - TValue[] values = _values; - _hashTable = FrozenHashTable.Create( - incoming, - pair => pair.Key, - (index, pair) => values[index] = pair.Value); - } - - private static KeyValuePair[] MakeUniqueArray(IEnumerable> pairs) - { - EqualityComparer comp = EqualityComparer.Default; - if (!(pairs is Dictionary dict && dict.Comparer == comp)) - { - dict = new Dictionary(comp); - foreach (KeyValuePair pair in pairs) - { - dict[pair.Key] = pair.Value; - } - } - - if (dict.Count == 0) - { - return Array.Empty>(); - } - - var result = new KeyValuePair[dict.Count]; - ((ICollection>)dict).CopyTo(result, 0); - - return result; - } - - /// - public FrozenList Keys => new(_hashTable.HashCodes); - - /// - public FrozenList Values => new(_values); - - /// - public FrozenPairEnumerator GetEnumerator() => new(_hashTable.HashCodes, _values); - - /// - /// Gets an enumeration of the dictionary's keys. - /// - IEnumerable IReadOnlyDictionary.Keys => Count > 0 ? _hashTable.HashCodes : Array.Empty(); - - /// - /// Gets an enumeration of the dictionary's values. - /// - IEnumerable IReadOnlyDictionary.Values => Count > 0 ? _values : Array.Empty(); - - /// - /// Gets an enumeration of the dictionary's key/value pairs. - /// - /// The enumerator. - IEnumerator> IEnumerable>.GetEnumerator() - => Count > 0 ? GetEnumerator() : ((IList>)Array.Empty>()).GetEnumerator(); - - /// - /// Gets an enumeration of the dictionary's key/value pairs. - /// - /// The enumerator. - IEnumerator IEnumerable.GetEnumerator() => Count > 0 ? GetEnumerator() : ((IList>)Array.Empty>()).GetEnumerator(); - - /// - /// Gets the number of key/value pairs in the dictionary. - /// - public int Count => _hashTable.Count; - - /// - /// Gets the value associated to the given key. - /// - /// The key to lookup. - /// The associated value. - /// If the key doesn't exist in the dictionary. - public TValue this[int key] - { - get - { - _hashTable.FindMatchingEntries(key, out int index, out int endIndex); - - while (index <= endIndex) - { - if (key == _hashTable.EntryHashCode(index)) - { - return _values[index]; - } - - index++; - } - - throw new KeyNotFoundException(); - } - } - - /// - /// Checks whether a particular key exists in the dictionary. - /// - /// The key to probe for. - /// if the key is in the dictionary, otherwise . - public bool ContainsKey(int key) - { - _hashTable.FindMatchingEntries(key, out int index, out int endIndex); - - while (index <= endIndex) - { - if (key == _hashTable.EntryHashCode(index)) - { - return true; - } - - index++; - } - - return false; - } - - /// - /// Tries to get a value associated with a specific key. - /// - /// The key to lookup. - /// The value associated with the key. - /// if the key was found, otherwise . - public bool TryGetValue(int key, [MaybeNullWhen(false)] out TValue value) - { - _hashTable.FindMatchingEntries(key, out int index, out int endIndex); - - while (index <= endIndex) - { - if (key == _hashTable.EntryHashCode(index)) - { - value = _values[index]; - return true; - } - - index++; - } - - value = default!; - return false; - } - - /// - public ref readonly TValue GetByRef(int key) - { - _hashTable.FindMatchingEntries(key, out int index, out int endIndex); - - while (index <= endIndex) - { - if (key == _hashTable.EntryHashCode(index)) - { - return ref _values[index]; - } - - index++; - } - - throw new KeyNotFoundException(); - } - - /// - public ref readonly TValue TryGetByRef(int key) - { - _hashTable.FindMatchingEntries(key, out int index, out int endIndex); - - while (index <= endIndex) - { - if (key == _hashTable.EntryHashCode(index)) - { - return ref _values[index]; - } - - index++; - } - - return ref Unsafe.NullRef(); - } - - /// - /// Copies the content of the dictionary to a span. - /// - /// The destination where to copy to. - public void CopyTo(Span> destination) - { - ThrowHelper.IfBufferTooSmall(destination.Length, Count); - - for (int i = 0; i < Count; i++) - { - destination[i] = new KeyValuePair(_hashTable.HashCodes[i], _values[i]); - } - } - - /// - /// Copies the content of the dictionary to an array. - /// - /// The destination where to copy to. - /// Index into the array where to start copying the data. - public void CopyTo(KeyValuePair[] array, int arrayIndex) => CopyTo(array.AsSpan(arrayIndex)); - - /// - /// Gets a value indicating whether this collection is a read-only collection. - /// - /// Always returns true. - bool ICollection>.IsReadOnly => true; - - /// - /// Gets a collection holding this dictionary's keys. - /// - ICollection IDictionary.Keys => Count > 0 ? _hashTable.HashCodes : Array.Empty(); - - /// - /// Gets a collection holding this dictionary's values. - /// - ICollection IDictionary.Values => Count > 0 ? _values : Array.Empty(); - - /// - /// Determines whether the dictionary contains the given key/value pair. - /// - /// The item to search for. - /// if the item is in the dictionary, otherwise . - bool ICollection>.Contains(KeyValuePair item) - { - ref readonly TValue v = ref TryGetByRef(item.Key); - if (Unsafe.IsNullRef(ref Unsafe.AsRef(in v))) - { - return false; - } - - return EqualityComparer.Default.Equals(v, item.Value); - } - - [EditorBrowsable(EditorBrowsableState.Never)] - TValue IDictionary.this[int key] - { - get => this[key]; - set => throw new NotSupportedException(); - } - - [EditorBrowsable(EditorBrowsableState.Never)] - void ICollection>.Add(KeyValuePair item) => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - void ICollection>.Clear() => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - bool ICollection>.Remove(KeyValuePair item) => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - void IDictionary.Add(int key, TValue value) => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - bool IDictionary.Remove(int key) => throw new NotSupportedException(); - } -} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenIntSet.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenIntSet.cs deleted file mode 100644 index e8f7f6227e196..0000000000000 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenIntSet.cs +++ /dev/null @@ -1,218 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Diagnostics; - -namespace System.Collections.Immutable -{ - /// - /// A frozen set of integers. - /// - /// - /// Frozen sets are immutable and are optimized for situations where a set - /// is created infrequently, but used repeatedly at runtime. They have a relatively high - /// cost to create, but provide excellent lookup performance. These are thus ideal for cases - /// where a set is created at startup of an application and used throughout the life - /// of the application. - /// - [DebuggerTypeProxy(typeof(IReadOnlyCollectionDebugView))] - [DebuggerDisplay("Count = {Count}")] - public readonly struct FrozenIntSet : IFrozenSet, IFindItem, ISet - { - private readonly FrozenHashTable _hashTable; - - /// - /// Gets an empty frozen integer set. - /// - public static FrozenIntSet Empty => new(Array.Empty()); - - internal FrozenIntSet(IEnumerable items) - { - int[] incoming = MakeUniqueArray(items); - _hashTable = FrozenHashTable.Create( - incoming, - item => item, - (_, _) => { }); - } - - private static int[] MakeUniqueArray(IEnumerable items) - { - EqualityComparer comp = EqualityComparer.Default; - if (!(items is HashSet hs && hs.Comparer == comp)) - { - hs = new HashSet(items, comp); - } - - if (hs.Count == 0) - { - return Array.Empty(); - } - - int[] result = new int[hs.Count]; - hs.CopyTo(result); - - return result; - } - - /// - public FrozenList Items => new(_hashTable.HashCodes); - - /// - public FrozenEnumerator GetEnumerator() => new(_hashTable.HashCodes); - - /// - /// Gets an enumeration of the set's items. - /// - /// The enumerator. - IEnumerator IEnumerable.GetEnumerator() => Count > 0 ? GetEnumerator() : ((IList)Array.Empty()).GetEnumerator(); - - /// - /// Gets an enumeration of the set's items. - /// - /// The enumerator. - IEnumerator IEnumerable.GetEnumerator() => Count > 0 ? GetEnumerator() : ((IList)Array.Empty()).GetEnumerator(); - - /// - /// Gets the number of items in the set. - /// - public int Count => _hashTable.Count; - - /// - /// Checks whether an item is present in the set. - /// - /// The item to probe for. - /// if the item is in the set, otherwise. - public bool Contains(int item) - { - _hashTable.FindMatchingEntries(item, out int index, out int endIndex); - - while (index <= endIndex) - { - if (item == _hashTable.EntryHashCode(index)) - { - return true; - } - - index++; - } - - return false; - } - - /// - /// Looks up an item's index. - /// - /// The item to find. - /// The index of the item, or -1 if the item was not found. - int IFindItem.FindItemIndex(int item) - { - _hashTable.FindMatchingEntries(item, out int index, out int endIndex); - - while (index <= endIndex) - { - if (item == _hashTable.EntryHashCode(index)) - { - return index; - } - - index++; - } - - return -1; - } - - /// - /// Determines whether this set is a proper subset of the specified collection. - /// - /// The collection to compare. - /// if the set is a proper subset of ; otherwise, . - /// If is . - public bool IsProperSubsetOf(IEnumerable other) => SetSupport.IsProperSubsetOf(this, other); - - /// - /// Determines whether this set is a proper superset of the specified collection. - /// - /// The collection to compare. - /// if the set is a proper superset of ; otherwise, . - /// If is . - public bool IsProperSupersetOf(IEnumerable other) => SetSupport.IsProperSupersetOf(this, other); - - /// - /// Determines whether this set is a subset of the specified collection. - /// - /// The collection to compare. - /// if the set is a subset of ; otherwise, . - /// If is . - public bool IsSubsetOf(IEnumerable other) => SetSupport.IsSubsetOf(this, other); - - /// - /// Determines whether this set is a superset of the specified collection. - /// - /// The collection to compare. - /// if the set is a superset of ; otherwise, . - /// If is . - public bool IsSupersetOf(IEnumerable other) => SetSupport.IsSupersetOf(this, other); - - /// - /// Determines whether this set shares any elements with the specified collection. - /// - /// The collection to compare. - /// if the set and the collection share at least one element; otherwise, . - /// If is . - public bool Overlaps(IEnumerable other) => SetSupport.Overlaps(this, other); - - /// - /// Determines whether this set and collection contain the same elements. - /// - /// The collection to compare. - /// if the set and the collection contains the exact same elements; otherwise, . - /// If is . - public bool SetEquals(IEnumerable other) => SetSupport.SetEquals(this, other); - - /// - /// Copies the content of the set to a span. - /// - /// The destination where to copy to. - public void CopyTo(Span destination) => _hashTable.HashCodes.AsSpan().CopyTo(destination); - - /// - /// Copies the content of the set to an array. - /// - /// The destination where to copy to. - /// Index into the array where to start copying the data. - public void CopyTo(int[] array, int arrayIndex) => CopyTo(array.AsSpan(arrayIndex)); - - /// - /// Gets a value indicating whether this collection is a read-only collection. - /// - /// Always returns true. - bool ICollection.IsReadOnly => true; - - [EditorBrowsable(EditorBrowsableState.Never)] - void ICollection.Add(int item) => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - void ICollection.Clear() => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - bool ICollection.Remove(int item) => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - bool ISet.Add(int item) => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - void ISet.ExceptWith(IEnumerable other) => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - void ISet.IntersectWith(IEnumerable other) => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - void ISet.SymmetricExceptWith(IEnumerable other) => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - void ISet.UnionWith(IEnumerable other) => throw new NotSupportedException(); - } -} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenList.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenList.cs deleted file mode 100644 index e3ac870753413..0000000000000 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenList.cs +++ /dev/null @@ -1,129 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; - -namespace System.Collections.Immutable -{ - /// - /// A simple frozen list of items. - /// - /// The item's type. - /// - /// This type is a slight improvement over the classic . It uses less memory - /// and enumerates items a bit faster. - /// - [DebuggerTypeProxy(typeof(IReadOnlyCollectionDebugView<>))] - [DebuggerDisplay("Count = {Count}")] - public readonly struct FrozenList : IReadOnlyList, ICollection - { - private readonly T[] _items; - - /// - /// Gets an empty frozen list. - /// - public static FrozenList Empty { get; } = Array.Empty().ToFrozenList(); - - /// - /// Initializes a new instance of the struct. - /// - /// The items to track in the list. - /// - /// Note that this takes a reference to the incoming array and does not copy it. This means that mutating this - /// array over time will also affect the items that this frozen collection returns. - /// - internal FrozenList(T[] items) - { - _items = items; - } - - internal FrozenList(IEnumerable items) - { - if (items is ICollection c) - { - T[] result = new T[c.Count]; - c.CopyTo(result, 0); - _items = result; - } - else - { - _items = new List(items).ToArray(); - } - } - - /// - /// Gets the element at the specified index in the list. - /// - /// The zero-based index of the element to get. - public T this[int index] => _items[index]; - - /// - /// Gets the number of items in the list. - /// - public int Count => _items.Length; - - /// - /// Gets a span of the items in the list. - /// - /// The span of items. - public ReadOnlySpan AsSpan() => _items.AsSpan(); - - /// - /// Returns an enumerator that iterates through the list. - /// - /// - /// An enumerator that can be used to iterate through the list. - /// - public FrozenEnumerator GetEnumerator() => new(_items); - - /// - /// Gets an enumeration of this list's items. - /// - /// The enumerator. - IEnumerator IEnumerable.GetEnumerator() => Count > 0 ? GetEnumerator() : ((IList)Array.Empty()).GetEnumerator(); - - /// - /// Gets an enumeration of this list's items. - /// - /// The enumerator. - IEnumerator IEnumerable.GetEnumerator() => Count > 0 ? GetEnumerator() : Array.Empty().GetEnumerator(); - - /// - /// Copies the content of the list to a span. - /// - /// The destination where to copy to. - public void CopyTo(Span destination) => _items.AsSpan().CopyTo(destination); - - /// - /// Copies the content of the list to an array. - /// - /// The destination where to copy to. - /// Index into the array where to start copying the data. - public void CopyTo(T[] array, int arrayIndex) => CopyTo(array.AsSpan(arrayIndex)); - - /// - /// Determines whether the list contains the given item. - /// - /// The item to search for. - /// if the item is in the list, otherwise . - /// This performs a slow linear scan through all the items and compares them using . - public bool Contains(T item) => Array.IndexOf(_items, item) >= 0; - - /// - /// Gets a value indicating whether this collection is a read-only collection. - /// - /// Always returns true. - bool ICollection.IsReadOnly => true; - - [EditorBrowsable(EditorBrowsableState.Never)] - void ICollection.Add(T item) => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - void ICollection.Clear() => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - bool ICollection.Remove(T item) => throw new NotSupportedException(); - } -} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenOrdinalStringDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenOrdinalStringDictionary.cs deleted file mode 100644 index 75887d8bd1afa..0000000000000 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenOrdinalStringDictionary.cs +++ /dev/null @@ -1,347 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - -namespace System.Collections.Immutable -{ - /// - /// A frozen dictionary with string keys compared with ordinal semantics. - /// - /// The type of the values in the dictionary. - /// - /// Frozen dictionaries are immutable and are optimized for situations where a dictionary - /// is created infrequently, but used repeatedly at runtime. They have a relatively high - /// cost to create, but provide excellent lookup performance. These are thus ideal for cases - /// where a dictionary is created at startup of an application and used throughout the life - /// of the application. - /// - [DebuggerTypeProxy(typeof(IFrozenOrdinalStringDictionaryDebugView<>))] - [DebuggerDisplay("Count = {Count}")] - public readonly struct FrozenOrdinalStringDictionary : IFrozenDictionary, IDictionary - { - private readonly FrozenHashTable _hashTable; - private readonly string[] _keys; - private readonly TValue[] _values; - private readonly StringComparerBase _comparer; - - /// - /// Gets an empty frozen string dictionary. - /// - public static FrozenOrdinalStringDictionary Empty => new(Array.Empty>()); - - /// - /// Initializes a new instance of the struct. - /// - /// The pairs to initialize the dictionary with. - /// Whether to use case-insensitive semantics. - /// If more than 64K pairs are added. - /// - /// Tf the same key appears multiple times in the input, the latter one in the sequence takes precedence. - /// - internal FrozenOrdinalStringDictionary(IEnumerable> pairs, bool ignoreCase = false) - { - KeyValuePair[] incoming = MakeUniqueArray(pairs, ignoreCase); - - _keys = incoming.Length == 0 ? Array.Empty() : new string[incoming.Length]; - _values = incoming.Length == 0 ? Array.Empty() : new TValue[incoming.Length]; - _comparer = ComparerPicker.Pick(SetSupport.ExtractStringKeysToArray(incoming), ignoreCase); - - string[] keys = _keys; - TValue[] values = _values; - StringComparerBase comparer = _comparer; - _hashTable = FrozenHashTable.Create( - incoming, - pair => comparer.GetHashCode(pair.Key), - (index, pair) => - { - keys[index] = pair.Key; - values[index] = pair.Value; - }); - } - - private static KeyValuePair[] MakeUniqueArray(IEnumerable> pairs, bool ignoreCase) - { - StringComparer comp = ignoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; - if (!(pairs is Dictionary dict && dict.Comparer.Equals(comp))) - { - dict = new Dictionary(comp); - foreach (KeyValuePair pair in pairs) - { - dict[pair.Key] = pair.Value; - } - } - - if (dict.Count == 0) - { - return Array.Empty>(); - } - - var result = new KeyValuePair[dict.Count]; - ((ICollection>)dict).CopyTo(result, 0); - - return result; - } - - /// - public FrozenList Keys => new(_keys); - - /// - public FrozenList Values => new(_values); - - /// - public FrozenPairEnumerator GetEnumerator() => new(_keys, _values); - - /// - /// Gets an enumeration of the dictionary's keys. - /// - IEnumerable IReadOnlyDictionary.Keys => Count > 0 ? _keys : Array.Empty(); - - /// - /// Gets an enumeration of the dictionary's values. - /// - IEnumerable IReadOnlyDictionary.Values => Count > 0 ? _values : Array.Empty(); - - /// - /// Gets an enumeration of the dictionary's key/value pairs. - /// - /// The enumerator. - IEnumerator> IEnumerable>.GetEnumerator() - => Count > 0 ? GetEnumerator() : ((IList>)Array.Empty>()).GetEnumerator(); - - /// - /// Gets an enumeration of the dictionary's key/value/pairs. - /// - /// The enumerator. - IEnumerator IEnumerable.GetEnumerator() => Count > 0 ? GetEnumerator() : Array.Empty>().GetEnumerator(); - - /// - /// Gets the number of key/value pairs in the dictionary. - /// - public int Count => _hashTable.Count; - - /// - /// Gets the value associated to the given key. - /// - /// The key to lookup. - /// The associated value. - /// If the key doesn't exist in the dictionary. - public TValue this[string key] - { - get - { - if (!_comparer.TrivialReject(key)) - { - int hashCode = _comparer.GetHashCode(key); - _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); - - while (index <= endIndex) - { - if (hashCode == _hashTable.EntryHashCode(index)) - { - if (_comparer.Equals(key, _keys[index])) - { - return _values[index]; - } - } - - index++; - } - } - - throw new KeyNotFoundException(); - } - } - - /// - /// Checks whether a particular key exists in the dictionary. - /// - /// The key to probe for. - /// if the key is in the dictionary, otherwise . - public bool ContainsKey(string key) - { - if (!_comparer.TrivialReject(key)) - { - int hashCode = _comparer.GetHashCode(key); - _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); - - while (index <= endIndex) - { - if (hashCode == _hashTable.EntryHashCode(index)) - { - if (_comparer.Equals(key, _keys[index])) - { - return true; - } - } - - index++; - } - } - - return false; - } - - /// - /// Tries to get a value associated with a specific key. - /// - /// The key to lookup. - /// The value associated with the key. - /// if the key was found, otherwise . - public bool TryGetValue(string key, [MaybeNullWhen(false)] out TValue value) - { - if (!_comparer.TrivialReject(key)) - { - int hashCode = _comparer.GetHashCode(key); - _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); - - while (index <= endIndex) - { - if (hashCode == _hashTable.EntryHashCode(index)) - { - if (_comparer.Equals(key, _keys[index])) - { - value = _values[index]; - return true; - } - } - - index++; - } - } - - value = default!; - return false; - } - - /// - public ref readonly TValue GetByRef(string key) - { - if (!_comparer.TrivialReject(key)) - { - int hashCode = _comparer.GetHashCode(key); - _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); - - while (index <= endIndex) - { - if (hashCode == _hashTable.EntryHashCode(index)) - { - if (_comparer.Equals(key, _keys[index])) - { - return ref _values[index]; - } - } - - index++; - } - } - - throw new KeyNotFoundException(); - } - - /// - public ref readonly TValue TryGetByRef(string key) - { - if (!_comparer.TrivialReject(key)) - { - int hashCode = _comparer.GetHashCode(key); - _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); - - while (index <= endIndex) - { - if (hashCode == _hashTable.EntryHashCode(index)) - { - if (_comparer.Equals(key, _keys[index])) - { - return ref _values[index]; - } - } - - index++; - } - } - - return ref Unsafe.NullRef(); - } - - /// - /// Copies the content of the dictionary to a span. - /// - /// The destination where to copy to. - public void CopyTo(Span> destination) - { - ThrowHelper.IfBufferTooSmall(destination.Length, Count); - - for (int i = 0; i < Count; i++) - { - destination[i] = new KeyValuePair(_keys[i], _values[i]); - } - } - - /// - /// Copies the content of the dictionary to an array. - /// - /// The destination where to copy to. - /// Index into the array where to start copying the data. - public void CopyTo(KeyValuePair[] array, int arrayIndex) => CopyTo(array.AsSpan(arrayIndex)); - - /// - /// Gets a value indicating whether this collection is a read-only collection. - /// - /// Always returns true. - bool ICollection>.IsReadOnly => true; - - /// - /// Gets a collection holding this dictionary's keys. - /// - ICollection IDictionary.Keys => Count > 0 ? _keys : Array.Empty(); - - /// - /// Gets a collection holding this dictionary's values. - /// - ICollection IDictionary.Values => Count > 0 ? _values : Array.Empty(); - - /// - /// Determines whether the dictionary contains the given key/value pair. - /// - /// The item to search for. - /// if the item is in the dictionary, otherwise . - bool ICollection>.Contains(KeyValuePair item) - { - ref readonly TValue v = ref TryGetByRef(item.Key); - if (Unsafe.IsNullRef(ref Unsafe.AsRef(in v))) - { - return false; - } - - return EqualityComparer.Default.Equals(v, item.Value); - } - - [EditorBrowsable(EditorBrowsableState.Never)] - TValue IDictionary.this[string key] - { - get => this[key]; - set => throw new NotSupportedException(); - } - - [EditorBrowsable(EditorBrowsableState.Never)] - void ICollection>.Add(KeyValuePair item) => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - void ICollection>.Clear() => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - bool ICollection>.Remove(KeyValuePair item) => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - void IDictionary.Add(string key, TValue value) => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - bool IDictionary.Remove(string key) => throw new NotSupportedException(); - } -} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenOrdinalStringSet.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenOrdinalStringSet.cs deleted file mode 100644 index ba744068fd4c4..0000000000000 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenOrdinalStringSet.cs +++ /dev/null @@ -1,247 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Diagnostics; - -namespace System.Collections.Immutable -{ - /// - /// A frozen set with strings compared with ordinal semantics. - /// - /// - /// Frozen sets are immutable and are optimized for situations where a set - /// is created infrequently, but used repeatedly at runtime. They have a relatively high - /// cost to create, but provide excellent lookup performance. These are thus ideal for cases - /// where a set is created at startup of an application and used throughout the life - /// of the application. - /// - [DebuggerTypeProxy(typeof(IReadOnlyCollectionDebugView))] - [DebuggerDisplay("Count = {Count}")] - public readonly struct FrozenOrdinalStringSet : IFrozenSet, IFindItem, ISet - { - private readonly FrozenHashTable _hashTable; - private readonly string[] _items; - - /// - /// Gets an empty frozen string set. - /// - public static FrozenOrdinalStringSet Empty => new(Array.Empty()); - - /// - /// Initializes a new instance of the struct. - /// - /// The items to initialize the set with. - /// Whether to use case-insensitive semantics. - /// If more than 64K items are added. - internal FrozenOrdinalStringSet(IEnumerable items, bool ignoreCase = false) - { - string[] incoming = MakeUniqueArray(items, ignoreCase); - - _items = incoming.Length == 0 ? Array.Empty() : new string[incoming.Length]; - Comparer = ComparerPicker.Pick(incoming, ignoreCase); - - string[] it = _items; - StringComparerBase comparer = Comparer; - _hashTable = FrozenHashTable.Create( - incoming, - comparer.GetHashCode, - (index, item) => it[index] = item); - } - - private static string[] MakeUniqueArray(IEnumerable items, bool ignoreCase) - { - StringComparer comp = ignoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; - if (!(items is HashSet hs && hs.Comparer.Equals(comp))) - { - hs = new HashSet(items, comp); - } - - if (hs.Count == 0) - { - return Array.Empty(); - } - - string[] result = new string[hs.Count]; - hs.CopyTo(result); - - return result; - } - - /// - public FrozenList Items => new(_items); - - /// - public FrozenEnumerator GetEnumerator() => new(_items); - - /// - /// Gets an enumeration of the set's items. - /// - /// The enumerator. - IEnumerator IEnumerable.GetEnumerator() => Count > 0 ? GetEnumerator() : ((IList)Array.Empty()).GetEnumerator(); - - /// - /// Gets an enumeration of the set's items. - /// - /// The enumerator. - IEnumerator IEnumerable.GetEnumerator() => Count > 0 ? GetEnumerator() : Array.Empty().GetEnumerator(); - - /// - /// Gets the number of items in the set. - /// - public int Count => _hashTable.Count; - - internal StringComparerBase Comparer { get; } - - /// - /// Checks whether an item is present in the set. - /// - /// The item to probe for. - /// if the item is in the set, otherwise. - public bool Contains(string item) - { - if (!Comparer.TrivialReject(item)) - { - int hashCode = Comparer.GetHashCode(item); - _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); - - while (index <= endIndex) - { - if (hashCode == _hashTable.EntryHashCode(index)) - { - if (Comparer.Equals(item, _items[index])) - { - return true; - } - } - - index++; - } - } - - return false; - } - - /// - /// Looks up an item's index. - /// - /// The item to find. - /// The index of the item, or -1 if the item was not found. - int IFindItem.FindItemIndex(string item) - { - if (!Comparer.TrivialReject(item)) - { - int hashCode = Comparer.GetHashCode(item); - _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); - - while (index <= endIndex) - { - if (hashCode == _hashTable.EntryHashCode(index)) - { - if (Comparer.Equals(item, _items[index])) - { - return index; - } - } - - index++; - } - } - - return -1; - } - - /// - /// Determines whether this set is a proper subset of the specified collection. - /// - /// The collection to compare. - /// if the set is a proper subset of ; otherwise, . - /// If is . - public bool IsProperSubsetOf(IEnumerable other) => SetSupport.IsProperSubsetOf(this, other); - - /// - /// Determines whether this set is a proper superset of the specified collection. - /// - /// The collection to compare. - /// if the set is a proper superset of ; otherwise, . - /// If is . - public bool IsProperSupersetOf(IEnumerable other) => SetSupport.IsProperSupersetOf(this, other); - - /// - /// Determines whether this set is a subset of the specified collection. - /// - /// The collection to compare. - /// if the set is a subset of ; otherwise, . - /// If is . - public bool IsSubsetOf(IEnumerable other) => SetSupport.IsSubsetOf(this, other); - - /// - /// Determines whether this set is a superset of the specified collection. - /// - /// The collection to compare. - /// if the set is a superset of ; otherwise, . - /// If is . - public bool IsSupersetOf(IEnumerable other) => SetSupport.IsSupersetOf(this, other); - - /// - /// Determines whether this set shares any elements with the specified collection. - /// - /// The collection to compare. - /// if the set and the collection share at least one element; otherwise, . - /// If is . - public bool Overlaps(IEnumerable other) => SetSupport.Overlaps(this, other); - - /// - /// Determines whether this set and collection contain the same elements. - /// - /// The collection to compare. - /// if the set and the collection contains the exact same elements; otherwise, . - /// If is . - public bool SetEquals(IEnumerable other) => SetSupport.SetEquals(this, other); - - /// - /// Copies the content of the set to a span. - /// - /// The destination where to copy to. - public void CopyTo(Span destination) => _items.AsSpan().CopyTo(destination); - - /// - /// Copies the content of the set to an array. - /// - /// The destination where to copy to. - /// Index into the array where to start copying the data. - public void CopyTo(string[] array, int arrayIndex) => CopyTo(array.AsSpan(arrayIndex)); - - /// - /// Gets a value indicating whether this collection is a read-only collection. - /// - /// Always returns true. - bool ICollection.IsReadOnly => true; - - [EditorBrowsable(EditorBrowsableState.Never)] - void ICollection.Add(string item) => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - void ICollection.Clear() => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - bool ICollection.Remove(string item) => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - bool ISet.Add(string item) => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - void ISet.ExceptWith(IEnumerable other) => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - void ISet.IntersectWith(IEnumerable other) => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - void ISet.SymmetricExceptWith(IEnumerable other) => throw new NotSupportedException(); - - [EditorBrowsable(EditorBrowsableState.Never)] - void ISet.UnionWith(IEnumerable other) => throw new NotSupportedException(); - } -} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenPairEnumerator.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenPairEnumerator.cs deleted file mode 100644 index a21043f1bc70f..0000000000000 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenPairEnumerator.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; - -namespace System.Collections.Immutable -{ - /// - /// Enumerates the key/value pairs of a dictionary. - /// - /// The types of the dictionary's keys. - /// The types of the dictionary's values. - public struct FrozenPairEnumerator : IEnumerator> - { - private readonly TKey[] _keys; - private readonly TValue[] _values; - private int _index; - - internal FrozenPairEnumerator(TKey[] keys, TValue[] values) - { - _keys = keys; - _values = values; - _index = -1; - } - - /// - /// Gets the key/value pair at the current position of the enumerator. - /// - public readonly KeyValuePair Current - { - get - { - if (_index >= 0) - { - return new KeyValuePair(_keys[_index], _values[_index]); - } - - return Throw(); - } - } - - /// - /// Disposes the object. - /// - void IDisposable.Dispose() - { - // nothing to do - } - - /// - /// Advances the enumerator to the next key/value pair of the dictionary. - /// - /// if the enumerator was successfully advanced to the next pair; if the enumerator has passed the end of the dictionary. - public bool MoveNext() - { - if (_index < _keys.Length - 1) - { - _index++; - return true; - } - - return false; - } - - /// - /// Resets the enumerator to its initial state. - /// - void IEnumerator.Reset() => _index = -1; - - /// - /// Gets the current value held by the enumerator. - /// - object IEnumerator.Current => Current; - - // keep this separate to allow inlining of the Current property - private static KeyValuePair Throw() - { - throw new InvalidOperationException("Call MoveNext() before reading the Current property."); - } - } -} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenSet.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenSet.cs index 3ce7b7ab3e884..f8a838852785c 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenSet.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenSet.cs @@ -2,254 +2,352 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.ComponentModel; +using System.Collections.Immutable; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; -namespace System.Collections.Immutable +namespace System.Collections.Frozen { /// - /// A frozen set. + /// Provides a set of initialization methods for instances of the class. /// - /// The type of the items in the set. /// - /// Frozen sets are immutable and are optimized for situations where a set - /// is created infrequently, but used repeatedly at runtime. They have a relatively high - /// cost to create, but provide excellent lookup performance. These are thus ideal for cases - /// where a set is created at startup of an application and used throughout the life - /// of the application. - /// - /// This is the general-purpose frozen set which can be used with any item type. If you need - /// a set that has a string or integer as key, you will get better performance by using - /// or - /// respectively. + /// Frozen collections are immutable and are optimized for situations where a collection + /// is created very infrequently but is used very frequently at runtime. They have a relatively high + /// cost to create but provide excellent lookup performance. Thus, these are ideal for cases + /// where a collection is created once, potentially at the startup of an application, and used throughout + /// the remainder of the life of the application. Frozen collections should only be initialized with + /// trusted input. /// - [DebuggerTypeProxy(typeof(IReadOnlyCollectionDebugView<>))] - [DebuggerDisplay("Count = {Count}")] - public readonly struct FrozenSet : IFrozenSet, IFindItem, ISet - where T : notnull + public static class FrozenSet { - private readonly FrozenHashTable _hashTable; - private readonly T[] _items; - - /// - /// Gets an empty frozen set. - /// - public static FrozenSet Empty => new(Array.Empty(), EqualityComparer.Default); - - /// - /// Initializes a new instance of the struct. - /// - /// The items to initialize the set with. - /// The comparer used to compare and hash items. - /// If more than 64K items are added. - internal FrozenSet(IEnumerable items, IEqualityComparer comparer) + /// Creates a with the specified values. + /// The values to use to populate the set. + /// The comparer implementation to use to compare values for equality. If null, is used. + /// The type of the values in the set. + /// If the same key appears multiple times in the input, the latter one in the sequence takes precedence. + /// A frozen set. + public static FrozenSet ToFrozenSet(this IEnumerable source, IEqualityComparer? comparer = null) { - T[] incoming = MakeUniqueArray(items, comparer); + ThrowHelper.ThrowIfNull(source); + comparer ??= EqualityComparer.Default; - if (ReferenceEquals(comparer, StringComparer.Ordinal) || - ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase)) + // If the source is already frozen with the same comparer, it can simply be returned. + if (source is FrozenSet existing && + existing.Comparer.Equals(comparer)) { - comparer = (IEqualityComparer)ComparerPicker.Pick((string[])(object)incoming, ignoreCase: ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase)); + return existing; } - _items = incoming.Length == 0 ? Array.Empty() : new T[incoming.Length]; - Comparer = comparer; - - T[] it = _items; - _hashTable = FrozenHashTable.Create( - incoming, - comparer.GetHashCode, - (index, item) => it[index] = item); - } + // Ensure we have a HashSet<,> using the specified comparer such that all values + // are non-null and unique according to that comparer. + if (source is not HashSet uniqueValues || + (uniqueValues.Count != 0 && !uniqueValues.Comparer.Equals(comparer))) + { + uniqueValues = new HashSet(source, comparer); + } - private static T[] MakeUniqueArray(IEnumerable items, IEqualityComparer comp) - { - if (!(items is HashSet hs && hs.Comparer.Equals(comp))) + // If the input was empty, simply return the empty frozen set singleton. The comparer is ignored. + if (uniqueValues.Count == 0) { - hs = new HashSet(items, comp); + return FrozenSet.Empty; } - if (hs.Count == 0) + if (typeof(T).IsValueType) { - return Array.Empty(); + // Optimize for value types when the default comparer is being used. In such a case, the implementation + // may use EqualityComparer.Default.Equals/GetHashCode directly, with generic specialization enabling + // the Equals/GetHashCode methods to be devirtualized and possibly inlined. + if (ReferenceEquals(comparer, EqualityComparer.Default)) + { + // In the specific case of Int32 keys, we can optimize further to reduce memory consumption by using + // the underlying FrozenHashtable's Int32 index as the values themselves, avoiding the need to store the + // same values yet again. + return typeof(T) == typeof(int) ? + (FrozenSet)(object)new Int32FrozenSet((HashSet)(object)uniqueValues) : + new ValueTypeDefaultComparerFrozenSet(uniqueValues); + } } + else if (typeof(T) == typeof(string)) + { + // Null is rare as a value in the set and we don't optimize for it. This enables the ordinal string + // implementation to fast-path out on null inputs rather than having to accomodate null inputs. + if (!uniqueValues.Contains(default!)) + { + // If the value is a string and the comparer is known to provide ordinal (case-sensitive or case-insensitive) semantics, + // we can use an implementation that's able to examine and optimize based on lengths and/or subsequences within those strings. + if (ReferenceEquals(comparer, EqualityComparer.Default) || + ReferenceEquals(comparer, StringComparer.Ordinal) || + ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase)) + { + HashSet stringValues = (HashSet)(object)uniqueValues; + IEqualityComparer stringComparer = (IEqualityComparer)(object)comparer; + + FrozenSet frozenSet = + LengthBucketsFrozenSet.TryCreateLengthBucketsFrozenSet(stringValues, stringComparer) ?? + (FrozenSet)new OrdinalStringFrozenSet(stringValues, stringComparer); - var result = new T[hs.Count]; - hs.CopyTo(result); + return (FrozenSet)(object)frozenSet; + } + } + } - return result; + // No special-cases apply. Use the default frozen set. + return new DefaultFrozenSet(uniqueValues, comparer); } + } - /// - public FrozenList Items => new(_items); + /// Provides an immutable, read-only set optimized for fast lookup and enumeration. + /// The type of the values in this set. + /// + /// Frozen collections are immutable and are optimized for situations where a collection + /// is created very infrequently but is used very frequently at runtime. They have a relatively high + /// cost to create but provide excellent lookup performance. Thus, these are ideal for cases + /// where a collection is created once, potentially at the startup of an application, and used throughout + /// the remainder of the life of the application. Frozen collections should only be initialized with + /// trusted input. + /// + [DebuggerTypeProxy(typeof(ImmutableEnumerableDebuggerProxy<>))] + [DebuggerDisplay("Count = {Count}")] + public abstract class FrozenSet : ISet, +#if NET5_0_OR_GREATER + IReadOnlySet, +#endif + IReadOnlyCollection, ICollection + { + /// Initialize the set. + /// The comparer to use and to expose from . + private protected FrozenSet(IEqualityComparer comparer) => Comparer = comparer; - /// - public FrozenEnumerator GetEnumerator() => new(_items); - - /// - /// Gets an enumeration of the set's items. - /// - /// The enumerator. - IEnumerator IEnumerable.GetEnumerator() => Count > 0 ? GetEnumerator() : ((IList)Array.Empty()).GetEnumerator(); - - /// - /// Gets an enumeration of the set's items. - /// - /// The enumerator. - IEnumerator IEnumerable.GetEnumerator() => Count > 0 ? GetEnumerator() : Array.Empty().GetEnumerator(); - - /// - /// Gets the number of items in the set. - /// - public int Count => _hashTable.Count; - - /// - /// Gets the comparer used by this set. - /// + /// Gets an empty . + public static FrozenSet Empty { get; } = new EmptyFrozenSet(); + + /// Gets the comparer used by this set. public IEqualityComparer Comparer { get; } - /// - /// Checks whether an item is present in the set. - /// - /// The item to probe for. - /// if the item is in the set, otherwise. - public bool Contains(T item) + /// Gets a collection containing the values in the set. + /// The order of the values in the set is unspecified. + public ImmutableArray Items => ItemsCore; + + /// + private protected abstract ImmutableArray ItemsCore { get; } + + /// Gets the number of values contained in the set. + public int Count => CountCore; + + /// + private protected abstract int CountCore { get; } + + /// Copies the values in the set to an array, starting at the specified . + /// The array that is the destination of the values copied from the set. + /// The zero-based index in at which copying begins. + public void CopyTo(T[] destination, int destinationIndex) { - int hashCode = Comparer.GetHashCode(item); - _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); + ThrowHelper.ThrowIfNull(destination); + CopyTo(destination.AsSpan(destinationIndex)); + } - while (index <= endIndex) - { - if (hashCode == _hashTable.EntryHashCode(index)) - { - if (Comparer.Equals(item, _items[index])) - { - return true; - } - } + /// Copies the values in the set to a span. + /// The span that is the destination of the values copied from the set. + public void CopyTo(Span destination) => + Items.AsSpan().CopyTo(destination); - index++; + /// + void ICollection.CopyTo(Array array, int index) + { + if (array != null && array.Rank != 1) + { + throw new ArgumentException(SR.Arg_RankMultiDimNotSupported, nameof(array)); } - return false; + Array.Copy(Items.array!, 0, array!, index, Items.Length); } - /// - /// Looks up an item's index. - /// - /// The item to find. - /// The index of the item, or -1 if the item was not found. - int IFindItem.FindItemIndex(T item) - { - int hashCode = Comparer.GetHashCode(item); - _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); + /// + bool ICollection.IsReadOnly => true; - while (index <= endIndex) - { - if (hashCode == _hashTable.EntryHashCode(index)) - { - if (Comparer.Equals(item, _items[index])) - { - return index; - } - } + /// + bool ICollection.IsSynchronized => false; - index++; + /// + object ICollection.SyncRoot => this; + + /// Determines whether the set contains the specified element. + /// The element to locate. + /// if the set contains the specified element; otherwise, . + public bool Contains(T item) => + FindItemIndex(item) >= 0; + + /// Searches the set for a given value and returns the equal value it finds, if any. + /// The value to search for. + /// The value from the set that the search found, or the default value of T when the search yielded no match. + /// A value indicating whether the search was successful. + public bool TryGetValue(T equalValue, [MaybeNullWhen(false)] out T actualValue) + { + int index = FindItemIndex(equalValue); + if (index >= 0) + { + actualValue = Items[index]; + return true; } - return -1; + actualValue = default; + return false; } - /// - /// Determines whether this set is a proper subset of the specified collection. - /// - /// The collection to compare. - /// if the set is a proper subset of ; otherwise, . - /// If is . - public bool IsProperSubsetOf(IEnumerable other) => SetSupport.IsProperSubsetOf(this, other); - - /// - /// Determines whether this set is a proper superset of the specified collection. - /// - /// The collection to compare. - /// if the set is a proper superset of ; otherwise, . - /// If is . - public bool IsProperSupersetOf(IEnumerable other) => SetSupport.IsProperSupersetOf(this, other); - - /// - /// Determines whether this set is a subset of the specified collection. - /// - /// The collection to compare. - /// if the set is a subset of ; otherwise, . - /// If is . - public bool IsSubsetOf(IEnumerable other) => SetSupport.IsSubsetOf(this, other); - - /// - /// Determines whether this set is a superset of the specified collection. - /// - /// The collection to compare. - /// if the set is a superset of ; otherwise, . - /// If is . - public bool IsSupersetOf(IEnumerable other) => SetSupport.IsSupersetOf(this, other); - - /// - /// Determines whether this set shares any elements with the specified collection. - /// - /// The collection to compare. - /// if the set and the collection share at least one element; otherwise, . - /// If is . - public bool Overlaps(IEnumerable other) => SetSupport.Overlaps(this, other); - - /// - /// Determines whether this set and collection contain the same elements. - /// - /// The collection to compare. - /// if the set and the collection contains the exact same elements; otherwise, . - /// If is . - public bool SetEquals(IEnumerable other) => SetSupport.SetEquals(this, other); - - /// - /// Copies the content of the set to a span. - /// - /// The destination where to copy to. - public void CopyTo(Span destination) => _items.AsSpan().CopyTo(destination); - - /// - /// Copies the content of the set to an array. - /// - /// The destination where to copy to. - /// Index into the array where to start copying the data. - public void CopyTo(T[] array, int arrayIndex) => CopyTo(array.AsSpan(arrayIndex)); - - /// - /// Gets a value indicating whether this collection is a read-only collection. - /// - /// Always returns true. - bool ICollection.IsReadOnly => true; + /// Finds the index of a specific value in a set. + /// The value to lookup. + /// The index of the value, or -1 if not found. + private protected abstract int FindItemIndex(T item); - [EditorBrowsable(EditorBrowsableState.Never)] - void ICollection.Add(T item) => throw new NotSupportedException(); + /// Returns an enumerator that iterates through the set. + /// An enumerator that iterates through the set. + public Enumerator GetEnumerator() => GetEnumeratorCore(); - [EditorBrowsable(EditorBrowsableState.Never)] - void ICollection.Clear() => throw new NotSupportedException(); + /// + private protected abstract Enumerator GetEnumeratorCore(); - [EditorBrowsable(EditorBrowsableState.Never)] - bool ICollection.Remove(T item) => throw new NotSupportedException(); + /// + IEnumerator IEnumerable.GetEnumerator() => + Count == 0 ? ((IList)Array.Empty()).GetEnumerator() : + GetEnumerator(); - [EditorBrowsable(EditorBrowsableState.Never)] + /// + IEnumerator IEnumerable.GetEnumerator() => + Count == 0 ? Array.Empty().GetEnumerator() : + GetEnumerator(); + + /// bool ISet.Add(T item) => throw new NotSupportedException(); - [EditorBrowsable(EditorBrowsableState.Never)] + /// void ISet.ExceptWith(IEnumerable other) => throw new NotSupportedException(); - [EditorBrowsable(EditorBrowsableState.Never)] + /// void ISet.IntersectWith(IEnumerable other) => throw new NotSupportedException(); - [EditorBrowsable(EditorBrowsableState.Never)] + /// void ISet.SymmetricExceptWith(IEnumerable other) => throw new NotSupportedException(); - [EditorBrowsable(EditorBrowsableState.Never)] + /// void ISet.UnionWith(IEnumerable other) => throw new NotSupportedException(); + + /// + void ICollection.Add(T item) => throw new NotSupportedException(); + + /// + void ICollection.Clear() => throw new NotSupportedException(); + + /// + bool ICollection.Remove(T item) => throw new NotSupportedException(); + + /// + public bool IsProperSubsetOf(IEnumerable other) + { + ThrowHelper.ThrowIfNull(other); + return IsProperSubsetOfCore(other); + } + + /// + private protected abstract bool IsProperSubsetOfCore(IEnumerable other); + + /// + public bool IsProperSupersetOf(IEnumerable other) + { + ThrowHelper.ThrowIfNull(other); + return IsProperSupersetOfCore(other); + } + + /// + private protected abstract bool IsProperSupersetOfCore(IEnumerable other); + + /// + public bool IsSubsetOf(IEnumerable other) + { + ThrowHelper.ThrowIfNull(other); + return IsSubsetOfCore(other); + } + + /// + private protected abstract bool IsSubsetOfCore(IEnumerable other); + + /// + public bool IsSupersetOf(IEnumerable other) + { + ThrowHelper.ThrowIfNull(other); + return IsSupersetOfCore(other); + } + + /// + private protected abstract bool IsSupersetOfCore(IEnumerable other); + + /// + public bool Overlaps(IEnumerable other) + { + ThrowHelper.ThrowIfNull(other); + return OverlapsCore(other); + } + + /// + private protected abstract bool OverlapsCore(IEnumerable other); + + /// + public bool SetEquals(IEnumerable other) + { + ThrowHelper.ThrowIfNull(other); + return SetEqualsCore(other); + } + + /// + private protected abstract bool SetEqualsCore(IEnumerable other); + + /// Enumerates the values of a . + public struct Enumerator : IEnumerator + { + private readonly T[] _entries; + private int _index; + + internal Enumerator(T[] entries) + { + _entries = entries; + _index = -1; + } + + /// + public bool MoveNext() + { + _index++; + if ((uint)_index < (uint)_entries.Length) + { + return true; + } + + _index = _entries.Length; + return false; + } + + /// + public readonly T Current + { + get + { + if ((uint)_index >= (uint)_entries.Length) + { + ThrowHelper.ThrowInvalidOperationException(); + } + + return _entries[_index]; + } + } + + /// + object IEnumerator.Current => Current!; + + /// + void IEnumerator.Reset() => _index = -1; + + /// + void IDisposable.Dispose() { } + } } } diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenSetInternalBase.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenSetInternalBase.cs new file mode 100644 index 0000000000000..eae06ec68f854 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenSetInternalBase.cs @@ -0,0 +1,280 @@ +// 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.Collections.Immutable; +using System.Diagnostics; + +namespace System.Collections.Frozen +{ + /// Provides an internal base class from which frozen set implementations may derive. + /// The primary purpose of this base type is to provide implementations of the various Is methods. + /// The type of values in the set. + /// + /// The type of a struct that implements the internal IGenericSpecializedWrapper and wraps this struct. + /// This is an optimization, to minimize the virtual calls necessary to implement these bulk operations. + /// + internal abstract class FrozenSetInternalBase : FrozenSet + where TThisWrapper : struct, FrozenSetInternalBase.IGenericSpecializedWrapper + { + /// A wrapper around this that enables access to important members without making virtual calls. + private readonly TThisWrapper _thisSet; + + protected FrozenSetInternalBase(IEqualityComparer comparer) : base(comparer) + { + _thisSet = default; + _thisSet.Store(this); + } + + /// + private protected override bool IsProperSubsetOfCore(IEnumerable other) + { + Debug.Assert(_thisSet.Count != 0, "EmptyFrozenSet should have been used."); + + if (other is ICollection otherAsCollection) + { + int otherCount = otherAsCollection.Count; + + if (otherCount == 0) + { + // No set is a proper subset of an empty set. + return false; + } + + // If the other is a set and is using the same equality comparer, the operation can be optimized. + if (other is IReadOnlySet otherAsSet && ComparersAreCompatible(otherAsSet)) + { + return _thisSet.Count < otherCount && IsSubsetOfSetWithCompatibleComparer(otherAsSet); + } + } + + // We couldn't take a fast path; do the full comparison. + (int uniqueCount, int unfoundCount) = CheckUniqueAndUnfoundElements(other, returnIfUnfound: false); + return uniqueCount == _thisSet.Count && unfoundCount > 0; + } + + /// + private protected override bool IsProperSupersetOfCore(IEnumerable other) + { + Debug.Assert(_thisSet.Count != 0, "EmptyFrozenSet should have been used."); + + if (other is ICollection otherAsCollection) + { + int otherCount = otherAsCollection.Count; + + if (otherCount == 0) + { + // If other is the empty set, then this is a superset (since we know this one isn't empty). + return true; + } + + // If the other is a set and is using the same equality comparer, the operation can be optimized. + if (other is IReadOnlySet otherAsSet && ComparersAreCompatible(otherAsSet)) + { + return _thisSet.Count > otherCount && ContainsAllElements(otherAsSet); + } + } + + // We couldn't take a fast path; do the full comparison. + (int uniqueCount, int unfoundCount) = CheckUniqueAndUnfoundElements(other, returnIfUnfound: true); + return uniqueCount < _thisSet.Count && unfoundCount == 0; + } + + /// + private protected override bool IsSubsetOfCore(IEnumerable other) + { + Debug.Assert(_thisSet.Count != 0, "EmptyFrozenSet should have been used."); + + // If the other is a set and is using the same equality comparer, the operation can be optimized. + if (other is IReadOnlySet otherAsSet && ComparersAreCompatible(otherAsSet)) + { + return _thisSet.Count <= otherAsSet.Count && IsSubsetOfSetWithCompatibleComparer(otherAsSet); + } + + // We couldn't take a fast path; do the full comparison. + (int uniqueCount, int unfoundCount) = CheckUniqueAndUnfoundElements(other, returnIfUnfound: false); + return uniqueCount == _thisSet.Count && unfoundCount >= 0; + } + + /// + private protected override bool IsSupersetOfCore(IEnumerable other) + { + Debug.Assert(_thisSet.Count != 0, "EmptyFrozenSet should have been used."); + + // Try to compute the answer based purely on counts. + if (other is ICollection otherAsCollection) + { + int otherCount = otherAsCollection.Count; + + // If other is the empty set then this is a superset. + if (otherCount == 0) + { + return true; + } + + // If the other is a set and is using the same equality comparer, the operation can be optimized. + if (other is IReadOnlySet otherAsSet && + otherCount > _thisSet.Count && + ComparersAreCompatible(otherAsSet)) + { + return false; + } + } + + return ContainsAllElements(other); + } + + /// + private protected override bool OverlapsCore(IEnumerable other) + { + Debug.Assert(_thisSet.Count != 0, "EmptyFrozenSet should have been used."); + + foreach (T element in other) + { + if (_thisSet.FindItemIndex(element) >= 0) + { + return true; + } + } + + return false; + } + + /// + private protected override bool SetEqualsCore(IEnumerable other) + { + Debug.Assert(_thisSet.Count != 0, "EmptyFrozenSet should have been used."); + + // If the other is a set and is using the same equality comparer, the operation can be optimized. + if (other is IReadOnlySet otherAsSet && ComparersAreCompatible(otherAsSet)) + { + return _thisSet.Count == otherAsSet.Count && ContainsAllElements(otherAsSet); + } + + // We couldn't take a fast path; do the full comparison. + (int uniqueCount, int unfoundCount) = CheckUniqueAndUnfoundElements(other, returnIfUnfound: true); + return uniqueCount == _thisSet.Count && unfoundCount == 0; + } + + private bool ComparersAreCompatible(IReadOnlySet other) => + other switch + { + HashSet hs => _thisSet.Comparer.Equals(hs.Comparer), + SortedSet ss => _thisSet.Comparer.Equals(ss.Comparer), + ImmutableHashSet ihs => _thisSet.Comparer.Equals(ihs.KeyComparer), + ImmutableSortedSet iss => _thisSet.Comparer.Equals(iss.KeyComparer), + FrozenSet fs => _thisSet.Comparer.Equals(fs.Comparer), + _ => false + }; + + /// + /// Determines counts that can be used to determine equality, subset, and superset. + /// + /// + /// This is only used when other is an IEnumerable and not a known set. If other is a set + /// these properties can be checked faster without use of marking because we can assume + /// other has no duplicates. + /// + /// The following count checks are performed by callers: + /// 1. Equals: checks if unfoundCount = 0 and uniqueFoundCount = _count; i.e. everything + /// in other is in this and everything in this is in other + /// 2. Subset: checks if unfoundCount >= 0 and uniqueFoundCount = _count; i.e. other may + /// have elements not in this and everything in this is in other + /// 3. Proper subset: checks if unfoundCount > 0 and uniqueFoundCount = _count; i.e + /// other must have at least one element not in this and everything in this is in other + /// 4. Proper superset: checks if unfound count = 0 and uniqueFoundCount strictly less + /// than _count; i.e. everything in other was in this and this had at least one element + /// not contained in other. + /// + private unsafe KeyValuePair CheckUniqueAndUnfoundElements(IEnumerable other, bool returnIfUnfound) + { + Debug.Assert(_thisSet.Count != 0, "EmptyFrozenSet should have been used."); + + const int BitsPerInt32 = 32; + int intArrayLength = (_thisSet.Count / BitsPerInt32) + 1; + + int[]? rentedArray = null; + Span seenItems = intArrayLength <= 256 ? + stackalloc int[256] : + (rentedArray = ArrayPool.Shared.Rent(intArrayLength)); + seenItems.Clear(); + + // Iterate through every item in the other collection. For each, if it's + // found in this set and hasn't yet been found in this set, track it. Otherwise, + // track that items in the other set weren't found in this one. + int unfoundCount = 0; // count of items in other not found in this + int uniqueFoundCount = 0; // count of unique items in other found in this + foreach (T item in other) + { + int index = _thisSet.FindItemIndex(item); + if (index >= 0) + { + if ((seenItems[index / BitsPerInt32] & (1 << index)) == 0) + { + // Item hasn't been seen yet. + seenItems[index / BitsPerInt32] |= 1 << index; + uniqueFoundCount++; + } + } + else + { + unfoundCount++; + if (returnIfUnfound) + { + break; + } + } + } + + if (rentedArray is not null) + { + ArrayPool.Shared.Return(rentedArray); + } + + return new KeyValuePair(uniqueFoundCount, unfoundCount); + } + + private bool ContainsAllElements(IEnumerable other) + { + foreach (T element in other) + { + if (_thisSet.FindItemIndex(element) < 0) + { + return false; + } + } + + return true; + } + + private bool IsSubsetOfSetWithCompatibleComparer(IReadOnlySet other) + { + foreach (T item in _thisSet) + { + if (!other.Contains(item)) + { + return false; + } + } + + return true; + } + + /// Used to enable generic specialization with reference types. + /// + /// The bulk Is operations may end up performing multiple operations on "this" set. + /// To avoid each of those incurring virtual dispatch to the derived type, the derived + /// type hands down a struct wrapper through which all calls are performed. This base + /// class uses that generic struct wrapper to specialize and devirtualize. + /// + internal interface IGenericSpecializedWrapper + { + void Store(FrozenSet @this); + int Count { get; } + int FindItemIndex(T item); + IEqualityComparer Comparer { get; } + Enumerator GetEnumerator(); + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IFindItem.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IFindItem.cs deleted file mode 100644 index a6c5fa5a90a86..0000000000000 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IFindItem.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System.Collections.Immutable -{ - /// - /// Makes it possible to lookup items in a set and get their index. - /// - /// The type of item in the set. - internal interface IFindItem - { - /// - /// Finds the index of a specific item in a set. - /// - /// The item to lookup. - /// The index of the item, or -1 if not found. - int FindItemIndex(T item); - } -} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IFrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IFrozenDictionary.cs deleted file mode 100644 index a8ed9e5b6f6fc..0000000000000 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IFrozenDictionary.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Runtime.CompilerServices; - -namespace System.Collections.Immutable -{ - /// - /// A fast read-only dictionary. - /// - /// The type of the keys in the dictionary. - /// The type of the values in this dictionary. - /// - /// Frozen dictionaries are optimized for highly efficient read access and dense - /// memory representation. For this, they trade off creation time. Creating a frozen - /// dictionary can take a relatively long time, making frozen dictionaries best suited - /// for long-lived dictionaries that will experience many lookups in their lifetime - /// in order to compensate for the cost of creation. - /// - public interface IFrozenDictionary : IReadOnlyDictionary - where TKey : notnull - { - /// - /// Gets a list containing the keys in the dictionary. - /// - /// - /// The order of the keys in the dictionary is unspecified, but it is the same order as the associated values returned by the property. - /// - new FrozenList Keys { get; } - - /// - /// Gets a list containing the values in the dictionary. - /// - /// - /// The order of the values in the dictionary is unspecified, but it is the same order as the associated keys returned by the property. - /// - new FrozenList Values { get; } - - /// - /// Returns an enumerator that iterates through the dictionary. - /// - /// An enumerator that makes it possible to iterate through the dictionary's entries. - new FrozenPairEnumerator GetEnumerator(); - - /// - /// Gets a reference to a value in the dictionary. - /// - /// The key to lookup. - /// A reference to the value associated with the key. - /// If the specifed key doesn't exist in the dictionary.. - ref readonly TValue GetByRef(TKey key); - - /// - /// Gets a reference to a value in the dictionary. - /// - /// The key to lookup. - /// A reference to the value associated with the key, or a null reference if the specified key doesn't exist in the dictionary. - /// Use to test for the null reference return. - ref readonly TValue TryGetByRef(TKey key); - } -} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IFrozenDictionaryDebugView.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IFrozenDictionaryDebugView.cs deleted file mode 100644 index 636140ff37664..0000000000000 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IFrozenDictionaryDebugView.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Diagnostics; - -namespace System.Collections.Immutable -{ - internal sealed class IFrozenDictionaryDebugView - where TKey : notnull - { - private readonly IFrozenDictionary _dict; - - public IFrozenDictionaryDebugView(IFrozenDictionary dictionary) - { - _dict = dictionary; - } - - [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public KeyValuePair[] Items => new List>(_dict).ToArray(); - } -} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IFrozenIntDictionaryDebugView.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IFrozenIntDictionaryDebugView.cs deleted file mode 100644 index 2c2e2bd7be821..0000000000000 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IFrozenIntDictionaryDebugView.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Diagnostics; - -namespace System.Collections.Immutable -{ - internal sealed class IFrozenIntDictionaryDebugView - { - private readonly IFrozenDictionary _dict; - - public IFrozenIntDictionaryDebugView(IFrozenDictionary dictionary) - { - _dict = dictionary; - } - - [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public KeyValuePair[] Items => new List>(_dict).ToArray(); - } -} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IFrozenOrdinalStringDictionaryDebugView.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IFrozenOrdinalStringDictionaryDebugView.cs deleted file mode 100644 index b7c562c60f995..0000000000000 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IFrozenOrdinalStringDictionaryDebugView.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Diagnostics; - -namespace System.Collections.Immutable -{ - internal sealed class IFrozenOrdinalStringDictionaryDebugView - { - private readonly IFrozenDictionary _dict; - - public IFrozenOrdinalStringDictionaryDebugView(IFrozenDictionary dictionary) - { - _dict = dictionary; - } - - [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public KeyValuePair[] Items => new List>(_dict).ToArray(); - } -} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IFrozenSet.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IFrozenSet.cs deleted file mode 100644 index 67e958620bb2a..0000000000000 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IFrozenSet.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; - -namespace System.Collections.Immutable -{ - /// - /// A fast read-only set. - /// - /// The type of the items in the set. - /// - /// Frozen sets are optimized for highly efficient read access and dense - /// memory representation. For this, they trade off creation time. Creating a frozen - /// set can take a relatively long time, making frozen sets best suited - /// for long-lived sets that will experience many lookups in their lifetime - /// in order to compensate for the cost of creation. - /// - public interface IFrozenSet : IReadOnlySet - where T : notnull - { - /// - /// Gets a list containing the items in the set. - /// - /// - /// The order of the items returned does not correspond to the order in which the items were introduced into the set. - /// - FrozenList Items { get; } - - /// - /// Returns an enumerator that iterates through a set. - /// - /// An enumerator that makes it possible to iterate through a set's items. - new FrozenEnumerator GetEnumerator(); - } -} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IReadOnlyCollectionDebugView.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IReadOnlyCollectionDebugView.cs deleted file mode 100644 index 2cb5b32e1e0e0..0000000000000 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/IReadOnlyCollectionDebugView.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Diagnostics; - -namespace System.Collections.Immutable -{ - internal sealed class IReadOnlyCollectionDebugView - { - private readonly IReadOnlyCollection _collection; - - public IReadOnlyCollectionDebugView(IReadOnlyCollection collection) - { - _collection = collection; - } - - [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public T[] Items => new List(_collection).ToArray(); - } -} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Int32FrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Int32FrozenDictionary.cs new file mode 100644 index 0000000000000..8ff6dac1da1cb --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Int32FrozenDictionary.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.Collections.Frozen +{ + /// Provides a frozen dictionary to use when the key is an and the default comparer is used. + /// The type of the values in the dictionary. + /// + /// This key type is specialized as a memory optimization, as the frozen hash table already contains the array of all + /// int values, and we can thus use its array as the keys rather than maintaining a duplicate copy. + /// + internal sealed class Int32FrozenDictionary : FrozenDictionary + { + private readonly FrozenHashTable _hashTable; + private readonly TValue[] _values; + + internal Int32FrozenDictionary(Dictionary source) : base(EqualityComparer.Default) + { + Debug.Assert(source.Count != 0); + + KeyValuePair[] entries = new KeyValuePair[source.Count]; + ((ICollection>)source).CopyTo(entries, 0); + + _values = new TValue[entries.Length]; + + _hashTable = FrozenHashTable.Create( + entries, + pair => pair.Key, + (index, pair) => _values[index] = pair.Value); + } + + /// + private protected override ImmutableArray KeysCore => new ImmutableArray(_hashTable.HashCodes); + + /// + private protected override ImmutableArray ValuesCore => new ImmutableArray(_values); + + /// + private protected override Enumerator GetEnumeratorCore() => new Enumerator(_hashTable.HashCodes, _values); + + /// + private protected override int CountCore => _hashTable.Count; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected override ref readonly TValue GetValueRefOrNullRefCore(int key) + { + _hashTable.FindMatchingEntries(key, out int index, out int endIndex); + + while (index <= endIndex) + { + if (key == _hashTable.HashCodes[index]) + { + return ref _values[index]; + } + + index++; + } + + return ref Unsafe.NullRef(); + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Int32FrozenSet.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Int32FrozenSet.cs new file mode 100644 index 0000000000000..a0c9ab657d418 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Int32FrozenSet.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; + +namespace System.Collections.Frozen +{ + /// Provides a frozen set to use when the value is an and the default comparer is used. + internal sealed class Int32FrozenSet : FrozenSetInternalBase + { + private readonly FrozenHashTable _hashTable; + + internal Int32FrozenSet(HashSet source) : base(EqualityComparer.Default) + { + Debug.Assert(source.Count != 0); + + int[] entries = new int[source.Count]; + source.CopyTo(entries); + + _hashTable = FrozenHashTable.Create( + entries, + item => item, + (_, _) => { }); + } + + /// + private protected override ImmutableArray ItemsCore => new ImmutableArray(_hashTable.HashCodes); + + /// + private protected override Enumerator GetEnumeratorCore() => new Enumerator(_hashTable.HashCodes); + + /// + private protected override int CountCore => _hashTable.Count; + + /// + private protected override int FindItemIndex(int item) + { + _hashTable.FindMatchingEntries(item, out int index, out int endIndex); + + while (index <= endIndex) + { + if (item == _hashTable.HashCodes[index]) + { + return index; + } + + index++; + } + + return -1; + } + + internal struct GSW : IGenericSpecializedWrapper + { + private Int32FrozenSet _set; + public void Store(FrozenSet set) => _set = (Int32FrozenSet)set; + + public int Count => _set.Count; + public IEqualityComparer Comparer => _set.Comparer; + public int FindItemIndex(int item) => _set.FindItemIndex(item); + public Enumerator GetEnumerator() => _set.GetEnumerator(); + } + } +} 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 new file mode 100644 index 0000000000000..12485d4a1bf67 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ItemsFrozenSet.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; + +namespace System.Collections.Frozen +{ + /// Provides a base class for frozen sets that store their values in a dedicated array. + internal abstract class ItemsFrozenSet : FrozenSetInternalBase + where TThisWrapper : struct, FrozenSetInternalBase.IGenericSpecializedWrapper + { + private protected readonly FrozenHashTable _hashTable; + private protected readonly T[] _items; + + protected ItemsFrozenSet(HashSet source, IEqualityComparer comparer) : base(comparer) + { + Debug.Assert(source.Count != 0); + + T[] entries = new T[source.Count]; + source.CopyTo(entries); + + _items = new T[entries.Length]; + + _hashTable = FrozenHashTable.Create( + entries, + o => o is null ? 0 : comparer.GetHashCode(o), + (index, item) => _items[index] = item); + } + + /// + private protected sealed override ImmutableArray ItemsCore => new ImmutableArray(_items); + + /// + private protected sealed override Enumerator GetEnumeratorCore() => new Enumerator(_items); + + /// + private protected sealed override int CountCore => _hashTable.Count; + } +} 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 new file mode 100644 index 0000000000000..c6c699881abe5 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/KeysAndValuesFrozenDictionary.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; + +namespace System.Collections.Frozen +{ + /// Provides a base class for frozen dictionaries that store their keys and values in dedicated arrays. + internal abstract class KeysAndValuesFrozenDictionary : FrozenDictionary, IDictionary + where TKey : notnull + { + private protected readonly FrozenHashTable _hashTable; + private protected readonly TKey[] _keys; + private protected readonly TValue[] _values; + + protected KeysAndValuesFrozenDictionary(Dictionary source, IEqualityComparer comparer) : base(comparer) + { + Debug.Assert(source.Count != 0); + + KeyValuePair[] entries = new KeyValuePair[source.Count]; + ((ICollection>)source).CopyTo(entries, 0); + + _keys = new TKey[entries.Length]; + _values = new TValue[entries.Length]; + + _hashTable = FrozenHashTable.Create( + entries, + pair => comparer.GetHashCode(pair.Key), + (index, pair) => + { + _keys[index] = pair.Key; + _values[index] = pair.Value; + }); + } + + /// + private protected sealed override ImmutableArray KeysCore => new ImmutableArray(_keys); + + /// + private protected sealed override ImmutableArray ValuesCore => new ImmutableArray(_values); + + /// + private protected sealed override Enumerator GetEnumeratorCore() => new Enumerator(_keys, _values); + + /// + private protected sealed override int CountCore => _hashTable.Count; + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/LengthBucketsFrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/LengthBucketsFrozenDictionary.cs new file mode 100644 index 0000000000000..9f670de54c399 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/LengthBucketsFrozenDictionary.cs @@ -0,0 +1,150 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +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 KeyValuePair[][] _lengthBuckets; + private readonly int _minLength; + private readonly string[] _keys; + private readonly TValue[] _values; + private readonly bool _ignoreCase; + + private LengthBucketsFrozenDictionary( + string[] keys, TValue[] values, KeyValuePair[][] lengthBuckets, int minLength, IEqualityComparer comparer) : + base(comparer) + { + Debug.Assert(comparer == EqualityComparer.Default || comparer == StringComparer.Ordinal || comparer == StringComparer.OrdinalIgnoreCase); + + _keys = keys; + _values = values; + _lengthBuckets = lengthBuckets; + _minLength = minLength; + _ignoreCase = ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase); + } + + internal static LengthBucketsFrozenDictionary? TryCreateLengthBucketsFrozenSet(Dictionary source, IEqualityComparer comparer) + { + Debug.Assert(source.Count != 0); + Debug.Assert(comparer == EqualityComparer.Default || comparer == StringComparer.Ordinal || comparer == StringComparer.OrdinalIgnoreCase); + + // Iterate through all of the inputs, bucketing them based on the length of the string. + var groupedByLength = new Dictionary>>(); + int minLength = int.MaxValue, maxLength = int.MinValue; + foreach (KeyValuePair pair in source) + { + string s = pair.Key; + + if (s.Length < minLength) minLength = s.Length; + if (s.Length > maxLength) maxLength = s.Length; + + if (!groupedByLength.TryGetValue(s.Length, out List>? list)) + { + groupedByLength[s.Length] = list = new List>(MaxPerLength); + } + + // If we've already hit the max per-bucket limit, bail. + if (list.Count == MaxPerLength) + { + return null; + } + + list.Add(pair); + } + + // If there would be too much empty space in the lookup array, bail. + int spread = maxLength - minLength + 1; + if (groupedByLength.Count / (double)spread < EmptyLengthsRatio) + { + return null; + } + + string[] keys = new string[source.Count]; + TValue[] values = new TValue[keys.Length]; + var lengthBuckets = new KeyValuePair[maxLength - minLength + 1][]; + + // Iterate through each bucket, filling the keys/values arrays, and creating a lookup array such that + // given a string length we can index into that array to find the array of string,int pairs: the string + // is the key and the int is the index into the keys/values array for the corresponding entry. + int index = 0; + foreach (KeyValuePair>> group in groupedByLength) + { + KeyValuePair[] length = lengthBuckets[group.Key - minLength] = new KeyValuePair[group.Value.Count]; + int i = 0; + foreach (KeyValuePair pair in group.Value) + { + length[i] = new KeyValuePair(pair.Key, index); + keys[index] = pair.Key; + values[index] = pair.Value; + + i++; + index++; + } + } + + return new LengthBucketsFrozenDictionary(keys, values, lengthBuckets, minLength, comparer); + } + + /// + private protected override ImmutableArray KeysCore => new ImmutableArray(_keys); + + /// + private protected override ImmutableArray ValuesCore => new ImmutableArray(_values); + + /// + private protected override Enumerator GetEnumeratorCore() => new Enumerator(_keys, _values); + + /// + private protected override int CountCore => _keys.Length; + + /// + 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 length = key.Length - _minLength; + if (length >= 0) + { + // Get the bucket for this key's length. If it's null, the key isn't in the dictionary. + KeyValuePair[][] lengths = _lengthBuckets; + if ((uint)length < (uint)lengths.Length && lengths[length] is KeyValuePair[] subset) + { + // Now iterate through every key in the bucket to see whether this is a match. + if (_ignoreCase) + { + foreach (KeyValuePair kvp in subset) + { + if (StringComparer.OrdinalIgnoreCase.Equals(key, kvp.Key)) + { + return ref _values[kvp.Value]; + } + } + } + else + { + foreach (KeyValuePair kvp in subset) + { + if (key == kvp.Key) + { + return ref _values[kvp.Value]; + } + } + } + } + } + + return ref Unsafe.NullRef(); + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/LengthBucketsFrozenSet.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/LengthBucketsFrozenSet.cs new file mode 100644 index 0000000000000..716f3f904d752 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/LengthBucketsFrozenSet.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; + +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 KeyValuePair[][] _lengthBuckets; + private readonly int _minLength; + private readonly string[] _items; + private readonly bool _ignoreCase; + + private LengthBucketsFrozenSet(string[] items, KeyValuePair[][] lengthBuckets, int minLength, IEqualityComparer comparer) : + base(comparer) + { + Debug.Assert(comparer == EqualityComparer.Default || comparer == StringComparer.Ordinal || comparer == StringComparer.OrdinalIgnoreCase); + + _items = items; + _lengthBuckets = lengthBuckets; + _minLength = minLength; + _ignoreCase = ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase); + } + + internal static LengthBucketsFrozenSet? TryCreateLengthBucketsFrozenSet(HashSet source, IEqualityComparer comparer) + { + Debug.Assert(source.Count != 0); + Debug.Assert(comparer == EqualityComparer.Default || comparer == StringComparer.Ordinal || comparer == StringComparer.OrdinalIgnoreCase); + + // Iterate through all of the inputs, bucketing them based on the length of the string. + var groupedByLength = new Dictionary>(); + int minLength = int.MaxValue, maxLength = int.MinValue; + foreach (string s in source) + { + if (s.Length < minLength) minLength = s.Length; + if (s.Length > maxLength) maxLength = s.Length; + + if (!groupedByLength.TryGetValue(s.Length, out List? list)) + { + groupedByLength[s.Length] = list = new List(MaxPerLength); + } + + // If we've already hit the max per-bucket limit, bail. + if (list.Count == MaxPerLength) + { + return null; + } + + list.Add(s); + } + + // If there would be too much empty space in the lookup array, bail. + int spread = maxLength - minLength + 1; + if (groupedByLength.Count / (double)spread < EmptyLengthsRatio) + { + return null; + } + + string[] items = new string[source.Count]; + var lengthBuckets = new KeyValuePair[maxLength - minLength + 1][]; + + // Iterate through each bucket, filling the items array, and creating a lookup array such that + // given a string length we can index into that array to find the array of string,int pairs: the string + // is the key and the int is the index into the items array for the corresponding entry. + int index = 0; + foreach (KeyValuePair> group in groupedByLength) + { + KeyValuePair[] length = lengthBuckets[group.Key - minLength] = new KeyValuePair[group.Value.Count]; + int i = 0; + foreach (string value in group.Value) + { + length[i] = new KeyValuePair(value, index); + items[index] = value; + + i++; + index++; + } + } + + return new LengthBucketsFrozenSet(items, lengthBuckets, minLength, comparer); + } + + /// + private protected override ImmutableArray ItemsCore => new ImmutableArray(_items); + + /// + private protected override Enumerator GetEnumeratorCore() => new Enumerator(_items); + + /// + private protected override int CountCore => _items.Length; + + /// + private protected override int FindItemIndex(string item) + { + if (item is not null) // this implementation won't be used for null values + { + // If the length doesn't have an associated bucket, the key isn't in the set. + int length = item.Length - _minLength; + if (length >= 0) + { + // Get the bucket for this key's length. If it's null, the key isn't in the set. + KeyValuePair[][] lengths = _lengthBuckets; + if ((uint)length < (uint)lengths.Length && lengths[length] is KeyValuePair[] subset) + { + // Now iterate through every key in the bucket to see whether this is a match. + if (_ignoreCase) + { + foreach (KeyValuePair kvp in subset) + { + if (StringComparer.OrdinalIgnoreCase.Equals(item, kvp.Key)) + { + return kvp.Value; + } + } + } + else + { + foreach (KeyValuePair kvp in subset) + { + if (item == kvp.Key) + { + return kvp.Value; + } + } + } + } + } + } + + return -1; + } + + internal struct GSW : IGenericSpecializedWrapper + { + private LengthBucketsFrozenSet _set; + public void Store(FrozenSet set) => _set = (LengthBucketsFrozenSet)set; + + public int Count => _set.Count; + public IEqualityComparer Comparer => _set.Comparer; + public int FindItemIndex(string item) => _set.FindItemIndex(item); + public Enumerator GetEnumerator() => _set.GetEnumerator(); + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/OrdinalStringFrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/OrdinalStringFrozenDictionary.cs new file mode 100644 index 0000000000000..34265384492fe --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/OrdinalStringFrozenDictionary.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.Collections.Frozen +{ + /// Provides a frozen dictionary optimized for ordinal (case-sensitive or case-insensitive) lookup of strings. + /// The type of values in the dictionary. + internal sealed class OrdinalStringFrozenDictionary : FrozenDictionary + { + private readonly FrozenHashTable _hashTable; + private readonly string[] _keys; + private readonly TValue[] _values; + private readonly StringComparerBase _partialComparer; + private readonly int _minimumLength; + private readonly int _maximumLengthDiff; + + internal OrdinalStringFrozenDictionary(Dictionary source, IEqualityComparer comparer) : + base(comparer) + { + Debug.Assert(source.Count != 0); + Debug.Assert(comparer == EqualityComparer.Default || comparer == StringComparer.Ordinal || comparer == StringComparer.OrdinalIgnoreCase); + + var entries = new KeyValuePair[source.Count]; + ((ICollection>)source).CopyTo(entries, 0); + + _keys = new string[entries.Length]; + _values = new TValue[entries.Length]; + + _partialComparer = ComparerPicker.Pick( + Array.ConvertAll(entries, pair => pair.Key), + ignoreCase: ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase), + out _minimumLength, + out _maximumLengthDiff); + + _hashTable = FrozenHashTable.Create( + entries, + pair => _partialComparer.GetHashCode(pair.Key), + (index, pair) => + { + _keys[index] = pair.Key; + _values[index] = pair.Value; + }); + } + + /// + private protected override ImmutableArray KeysCore => new ImmutableArray(_keys); + + /// + private protected override ImmutableArray ValuesCore => new ImmutableArray(_values); + + /// + private protected override Enumerator GetEnumeratorCore() => new Enumerator(_keys, _values); + + /// + private protected override int CountCore => _hashTable.Count; + + /// + private protected override ref readonly TValue GetValueRefOrNullRefCore(string key) + { + if ((uint)(key.Length - _minimumLength) <= (uint)_maximumLengthDiff) + { + StringComparerBase partialComparer = _partialComparer; + + int hashCode = partialComparer.GetHashCode(key); + _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); + + while (index <= endIndex) + { + if (hashCode == _hashTable.HashCodes[index]) + { + if (partialComparer.Equals(key, _keys[index])) // partialComparer.Equals always compares the full input (EqualsPartial/GetHashCode don't) + { + return ref _values[index]; + } + } + + index++; + } + } + + return ref Unsafe.NullRef(); + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/OrdinalStringFrozenSet.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/OrdinalStringFrozenSet.cs new file mode 100644 index 0000000000000..0c6ff0d5bcee7 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/OrdinalStringFrozenSet.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; + +namespace System.Collections.Frozen +{ + /// Provides a frozen set optimized for ordinal (case-sensitive or case-insensitive) lookup of strings. + internal sealed class OrdinalStringFrozenSet : FrozenSetInternalBase + { + private readonly FrozenHashTable _hashTable; + private readonly string[] _items; + private readonly StringComparerBase _partialComparer; + private readonly int _minimumLength; + private readonly int _maximumLengthDiff; + + internal OrdinalStringFrozenSet(HashSet source, IEqualityComparer comparer) : + base(comparer) + { + Debug.Assert(source.Count != 0); + Debug.Assert(comparer == EqualityComparer.Default || comparer == StringComparer.Ordinal || comparer == StringComparer.OrdinalIgnoreCase); + + string[] entries = new string[source.Count]; + source.CopyTo(entries); + + _items = new string[entries.Length]; + + _partialComparer = ComparerPicker.Pick( + entries, + ignoreCase: ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase), + out _minimumLength, + out _maximumLengthDiff); + + _hashTable = FrozenHashTable.Create( + entries, + _partialComparer.GetHashCode, + (index, item) => _items[index] = item); + } + + /// + private protected override ImmutableArray ItemsCore => new ImmutableArray(_items); + + /// + private protected override Enumerator GetEnumeratorCore() => new Enumerator(_items); + + /// + private protected override int CountCore => _hashTable.Count; + + /// + private protected override int FindItemIndex(string item) + { + if (item is not null && // this implementation won't be used for null values + (uint)(item.Length - _minimumLength) <= (uint)_maximumLengthDiff) + { + StringComparerBase partialComparer = _partialComparer; + + int hashCode = partialComparer.GetHashCode(item); + _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); + + while (index <= endIndex) + { + if (hashCode == _hashTable.HashCodes[index]) + { + if (partialComparer.Equals(item, _items[index])) // partialComparer.Equals always compares the full input (EqualsPartial/GetHashCode don't) + { + return index; + } + } + + index++; + } + } + + return -1; + } + + internal struct GSW : IGenericSpecializedWrapper + { + private OrdinalStringFrozenSet _set; + public void Store(FrozenSet set) => _set = (OrdinalStringFrozenSet)set; + + public int Count => _set.Count; + public IEqualityComparer Comparer => _set.Comparer; + public int FindItemIndex(string item) => _set.FindItemIndex(item); + public Enumerator GetEnumerator() => _set.GetEnumerator(); + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/SetSupport.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/SetSupport.cs deleted file mode 100644 index 34005f2c48dc9..0000000000000 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/SetSupport.cs +++ /dev/null @@ -1,394 +0,0 @@ -// 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; - -namespace System.Collections.Immutable -{ - internal static class SetSupport - { - public static string[] ExtractStringKeysToArray(KeyValuePair[] source) where TKey : notnull - { - string[] keys = new string[source.Length]; - for (int i = 0; i < source.Length; i++) - { - keys[i] = (string)(object)source[i].Key; - } - return keys; - } - - public static bool IsProperSubsetOf(in TSet set, IEnumerable other) - where TSet : IFrozenSet, IFindItem - where T : notnull - { - Requires.NotNull(other, nameof(other)); - - if (other is ICollection otherAsCollection) - { - // No set is a proper subset of an empty set. - if (otherAsCollection.Count == 0) - { - return false; - } - - // The empty set is a proper subset of anything but the empty set. - if (set.Count == 0) - { - return otherAsCollection.Count > 0; - } - - // Faster if other is a hashset (and we're using same equality comparer). - if (other is IReadOnlySet otherAsSet && CompatibleComparers(set, otherAsSet)) - { - if (set.Count >= otherAsSet.Count) - { - return false; - } - - // This has strictly less than number of items in other, so the following - // check suffices for proper subset. - return IsSubsetOfHashSetWithCompatibleComparer(set, otherAsSet); - } - } - - (int uniqueCount, int unfoundCount) = CheckUniqueAndUnfoundElements(set, other, returnIfUnfound: false); - return uniqueCount == set.Count && unfoundCount > 0; - } - - public static bool IsProperSupersetOf(in TSet set, IEnumerable other) - where TSet : IFrozenSet, IFindItem - where T : notnull - { - Requires.NotNull(other, nameof(other)); - - // The empty set isn't a proper superset of any set, and a set is never a strict superset of itself. - if (set.Count == 0) - { - return false; - } - - if (other is ICollection otherAsCollection) - { - // If other is the empty set then this is a superset. - if (otherAsCollection.Count == 0) - { - // Note that this has at least one element, based on above check. - return true; - } - - // Faster if other is a hashset with the same equality comparer - if (other is IReadOnlySet otherAsSet && CompatibleComparers(set, otherAsSet)) - { - if (otherAsSet.Count >= set.Count) - { - return false; - } - - // Now perform element check. - return ContainsAllElements(set, otherAsSet); - } - } - - // Couldn't fall out in the above cases; do it the long way - (int uniqueCount, int unfoundCount) = CheckUniqueAndUnfoundElements(set, other, returnIfUnfound: true); - return uniqueCount < set.Count && unfoundCount == 0; - } - - public static bool IsSubsetOf(in TSet set, IEnumerable other) - where TSet : IFrozenSet, IFindItem - where T : notnull - { - Requires.NotNull(other, nameof(other)); - - // The empty set is a subset of any set, and a set is a subset of itself. - // Set is always a subset of itself - if (set.Count == 0) - { - return true; - } - - // Faster if other has unique elements according to this equality comparer; so check - // that other is a hashset using the same equality comparer. - if (other is IReadOnlySet otherAsSet && CompatibleComparers(set, otherAsSet)) - { - // if this has more elements then it can't be a subset - if (set.Count > otherAsSet.Count) - { - return false; - } - - // already checked that we're using same equality comparer. simply check that - // each element in this is contained in other. - return IsSubsetOfHashSetWithCompatibleComparer(set, otherAsSet); - } - - (int uniqueCount, int unfoundCount) = CheckUniqueAndUnfoundElements(set, other, returnIfUnfound: false); - return uniqueCount == set.Count && unfoundCount >= 0; - } - - public static bool IsSupersetOf(in TSet set, IEnumerable other) - where TSet : IFrozenSet - where T : notnull - { - Requires.NotNull(other, nameof(other)); - - // Try to fall out early based on counts. - if (other is ICollection otherAsCollection) - { - // If other is the empty set then this is a superset. - if (otherAsCollection.Count == 0) - { - return true; - } - - // Try to compare based on counts alone if other is a hashset with same equality comparer. - if (other is IReadOnlySet otherAsSet && - CompatibleComparers(set, otherAsSet) && - otherAsSet.Count > set.Count) - { - return false; - } - } - - return ContainsAllElements(set, other); - } - - public static bool Overlaps(in TSet set, IEnumerable other) - where TSet : IFrozenSet - where T : notnull - { - Requires.NotNull(other, nameof(other)); - - if (set.Count == 0) - { - return false; - } - - foreach (T element in other) - { - if (set.Contains(element)) - { - return true; - } - } - - return false; - } - - public static bool SetEquals(in TSet set, IEnumerable other) - where TSet : IFrozenSet, IFindItem - where T : notnull - { - Requires.NotNull(other, nameof(other)); - - // Faster if other is a hashset and we're using same equality comparer. - if (other is IReadOnlySet otherAsSet && CompatibleComparers(set, otherAsSet)) - { - // Attempt to return early: since both contain unique elements, if they have - // different counts, then they can't be equal. - if (set.Count != otherAsSet.Count) - { - return false; - } - - // Already confirmed that the sets have the same number of distinct elements, so if - // one is a superset of the other then they must be equal. - return ContainsAllElements(set, otherAsSet); - } - else - { - // If this count is 0 but other contains at least one element, they can't be equal. - if (set.Count == 0 && - other is ICollection otherAsCollection && - otherAsCollection.Count > 0) - { - return false; - } - - (int uniqueCount, int unfoundCount) = CheckUniqueAndUnfoundElements(set, other, returnIfUnfound: true); - return uniqueCount == set.Count && unfoundCount == 0; - } - } - - private static bool CompatibleComparers(in TSet set, IReadOnlySet other) - where TSet : IFrozenSet - where T : notnull - { - if (set is FrozenOrdinalStringSet foss) - { - if (other is HashSet hs) - { - if (foss.Comparer.CaseInsensitive) - { - return hs.Comparer.Equals(StringComparer.OrdinalIgnoreCase); - } - - return hs.Comparer.Equals(StringComparer.Ordinal); - } - else if (other is FrozenOrdinalStringSet otherfoss) - { - return foss.Comparer.CaseInsensitive == otherfoss.Comparer.CaseInsensitive; - } - } - else if (set is FrozenSet s) - { - if (other is HashSet hs) - { - return hs.Comparer == s.Comparer; - } - else if (other is FrozenSet fs) - { - return fs.Comparer == s.Comparer; - } - } - else - { - if (other is HashSet hs) - { - return hs.Comparer == EqualityComparer.Default; - } - - return other is FrozenIntSet; - } - - return false; - } - - /// - /// Determines counts that can be used to determine equality, subset, and superset. This - /// is only used when other is an IEnumerable and not a HashSet. If other is a HashSet - /// these properties can be checked faster without use of marking because we can assume - /// other has no duplicates. - /// - /// The following count checks are performed by callers: - /// 1. Equals: checks if unfoundCount = 0 and uniqueFoundCount = _count; i.e. everything - /// in other is in this and everything in this is in other - /// 2. Subset: checks if unfoundCount >= 0 and uniqueFoundCount = _count; i.e. other may - /// have elements not in this and everything in this is in other - /// 3. Proper subset: checks if unfoundCount > 0 and uniqueFoundCount = _count; i.e - /// other must have at least one element not in this and everything in this is in other - /// 4. Proper superset: checks if unfound count = 0 and uniqueFoundCount strictly less - /// than _count; i.e. everything in other was in this and this had at least one element - /// not contained in other. - /// - /// An earlier implementation used delegates to perform these checks rather than returning - /// an ElementCount struct; however this was changed due to the perf overhead of delegates. - /// - private static unsafe KeyValuePair CheckUniqueAndUnfoundElements(in TSet set, IEnumerable other, bool returnIfUnfound) - where TSet : IFrozenSet, IFindItem - where T : notnull - { - // Need special case for when this has no elements. - if (set.Count == 0) - { - int numElementsInOther = 0; - foreach (T item in other) - { - numElementsInOther++; - break; // break right away, all we want to know is whether other has 0 or 1 elements - } - - return new KeyValuePair(0, numElementsInOther); - } - - int originalCount = set.Count; - int intArrayLength = BitHelper.ToIntArrayLength(originalCount); - - int[]? rentedArray = null; - Span span = intArrayLength <= 128 ? - stackalloc int[128] : - (rentedArray = ArrayPool.Shared.Rent(intArrayLength)); - - var bitHelper = new BitHelper(span); - - int unfoundCount = 0; // count of items in other not found in this - int uniqueFoundCount = 0; // count of unique items in other found in this - - foreach (T item in other) - { - int index = set.FindItemIndex(item); - if (index >= 0) - { - if (!bitHelper.IsMarked(index)) - { - // Item hasn't been seen yet. - bitHelper.MarkBit(index); - uniqueFoundCount++; - } - } - else - { - unfoundCount++; - if (returnIfUnfound) - { - break; - } - } - } - - if (rentedArray is not null) - { - ArrayPool.Shared.Return(rentedArray); - } - - return new KeyValuePair(uniqueFoundCount, unfoundCount); - } - - private static bool ContainsAllElements(in TSet set, IEnumerable other) - where TSet : IFrozenSet - where T : notnull - { - foreach (T element in other) - { - if (!set.Contains(element)) - { - return false; - } - } - - return true; - } - - private static bool IsSubsetOfHashSetWithCompatibleComparer(in TSet set, IReadOnlySet other) - where TSet : IFrozenSet - where T : notnull - { - foreach (T item in set) - { - if (!other.Contains(item)) - { - return false; - } - } - - return true; - } - - private ref struct BitHelper - { - private const int IntSize = sizeof(int) * 8; - private readonly Span _span; - - internal BitHelper(Span span) - { - span.Clear(); - _span = span; - } - - internal static int ToIntArrayLength(int n) => ((n - 1) / IntSize) + 1; - - internal void MarkBit(int bitPosition) - { - int bitArrayIndex = bitPosition / IntSize; - _span[bitArrayIndex] |= 1 << (bitPosition % IntSize); - } - - internal bool IsMarked(int bitPosition) - { - int bitArrayIndex = bitPosition / IntSize; - return (_span[bitArrayIndex] & (1 << (bitPosition % IntSize))) != 0; - } - } - } -} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/ComparerPicker.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/ComparerPicker.cs index 70a87fdc5bb8c..2ea18d247f8c0 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/ComparerPicker.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/ComparerPicker.cs @@ -2,10 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; -namespace System.Collections.Immutable +namespace System.Collections.Frozen { internal static class ComparerPicker { @@ -15,7 +16,7 @@ internal static class ComparerPicker /// /// The idea here is to find the shortest substring slice across all the input strings which yields a set of /// strings which are maximally unique. The optimal slice is then applied to incoming strings being hashed to - /// perform the dictionary lookup. Keeping the slices as small as possible minimize the number of characters + /// perform the dictionary lookup. Keeping the slices as small as possible minimizes the number of characters /// involved in hashing, speeding up the whole process. /// /// What we do here is pretty simple. We loop over the input strings, looking for the shortest slice with a good @@ -27,39 +28,37 @@ internal static class ComparerPicker /// /// Warning: This code may reorganize (e.g. sort) the entries in the input array. It will not delete or add anything though. /// - public static StringComparerBase Pick(string[] uniqueStrings, bool ignoreCase) + public static StringComparerBase Pick(ReadOnlySpan uniqueStrings, bool ignoreCase, out int minimumLength, out int maximumLengthDiff) { - if (uniqueStrings.Length == 0) - { - return ignoreCase ? new FullCaseInsensitiveAsciiStringComparer() : new FullStringComparer(); - } - - // first, try to pick a substring comparer - StringComparerBase? c = PickSubstringComparer(uniqueStrings, ignoreCase); + Debug.Assert(uniqueStrings.Length != 0); - // if we couldn't find a good substring comparer, fallback to a full string comparer - c ??= PickFullStringComparer(uniqueStrings, ignoreCase); + // First, try to pick a substring comparer. + // if we couldn't find a good substring comparer, fallback to a full string comparer. + StringComparerBase? c = + PickSubstringComparer(uniqueStrings, ignoreCase) ?? + PickFullStringComparer(uniqueStrings, ignoreCase); - // calculate the trivial rejection boundaries - c.MinLength = int.MaxValue; - c.MaxLength = 0; + // Calculate the trivial rejection boundaries. + int min = int.MaxValue, max = 0; foreach (string s in uniqueStrings) { - if (s.Length < c.MinLength) + if (s.Length < min) { - c.MinLength = s.Length; + min = s.Length; } - if (s.Length > c.MaxLength) + if (s.Length > max) { - c.MaxLength = s.Length; + max = s.Length; } } + minimumLength = min; + maximumLengthDiff = max - min; return c; } - private static StringComparerBase? PickSubstringComparer(string[] uniqueStrings, bool ignoreCase) + private static StringComparerBase? PickSubstringComparer(ReadOnlySpan uniqueStrings, bool ignoreCase) { const double SufficientUniquenessFactor = 0.95; // 95% is good enough @@ -164,7 +163,7 @@ public static StringComparerBase Pick(string[] uniqueStrings, bool ignoreCase) return null; } - private static StringComparerBase PickFullStringComparer(string[] uniqueStrings, bool ignoreCase) + private static StringComparerBase PickFullStringComparer(ReadOnlySpan uniqueStrings, bool ignoreCase) { if (!ignoreCase) { @@ -186,36 +185,36 @@ private sealed class ComparerWrapper : IEqualityComparer { private readonly SubstringComparerBase _comp; - public ComparerWrapper(SubstringComparerBase comp) - { - _comp = comp; - } + public ComparerWrapper(SubstringComparerBase comp) => _comp = comp; public bool Equals(string? x, string? y) => _comp.EqualsPartial(x, y); public int GetHashCode([DisallowNull] string obj) => _comp.GetHashCode(obj); } + // TODO https://github.com/dotnet/runtime/issues/28230: + // Replace this once Ascii.IsValid exists. internal static unsafe bool IsAllAscii(ReadOnlySpan s) { fixed (char* src = s) { - uint* ptr = (uint*)src; + uint* ptrUInt32 = (uint*)src; int length = s.Length; while (length > 3) { - if (!AllCharsInUInt32AreAscii(*ptr++ | *ptr++)) + if (!AllCharsInUInt32AreAscii(ptrUInt32[0] | ptrUInt32[1])) { return false; } + ptrUInt32 += 2; length -= 4; } - char* tail = (char*)ptr; + char* ptrChar = (char*)ptrUInt32; while (length-- > 0) { - char ch = *tail++; + char ch = *ptrChar++; if (ch >= 0x7f) { return false; @@ -229,15 +228,15 @@ internal static unsafe bool IsAllAscii(ReadOnlySpan s) static bool AllCharsInUInt32AreAscii(uint value) => (value & ~0x007F_007Fu) == 0; } - private static double GetUniquenessFactor(HashSet set, IReadOnlyCollection uniqueStrings) + private static double GetUniquenessFactor(HashSet set, ReadOnlySpan uniqueStrings) { set.Clear(); foreach (string s in uniqueStrings) { - _ = set.Add(s); + set.Add(s); } - return set.Count / (double)uniqueStrings.Count; + return set.Count / (double)uniqueStrings.Length; } } } diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullCaseInsensitiveAsciiStringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullCaseInsensitiveAsciiStringComparer.cs index 0f96d696d9ee3..5c3b60a8b92f0 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullCaseInsensitiveAsciiStringComparer.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullCaseInsensitiveAsciiStringComparer.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace System.Collections.Immutable +namespace System.Collections.Frozen { /// /// A comparer for ordinal case-insensitive ascii-only string comparisons. @@ -14,7 +14,6 @@ namespace System.Collections.Immutable internal sealed class FullCaseInsensitiveAsciiStringComparer : StringComparerBase { public override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y); - public override int GetHashCode(string s) => Hashing.GetCaseInsensitiveAsciiHashCode(s.AsSpan()); - public override bool CaseInsensitive => true; + public override int GetHashCode(string s) => GetHashCodeOrdinalIgnoreCaseAscii(s.AsSpan()); } } diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullCaseInsensitiveStringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullCaseInsensitiveStringComparer.cs index 927a0062a302e..67add016b6c05 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullCaseInsensitiveStringComparer.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullCaseInsensitiveStringComparer.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace System.Collections.Immutable +namespace System.Collections.Frozen { /// /// A comparer for ordinal case-insensitive string comparisons. @@ -14,7 +14,6 @@ namespace System.Collections.Immutable internal sealed class FullCaseInsensitiveStringComparer : StringComparerBase { public override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y); - public override int GetHashCode(string s) => Hashing.GetCaseInsensitiveHashCode(s.AsSpan()); - public override bool CaseInsensitive => true; + public override int GetHashCode(string s) => GetHashCodeOrdinalIgnoreCase(s.AsSpan()); } } diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullStringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullStringComparer.cs index 75437c7d98e82..1c9ead8804de0 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullStringComparer.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullStringComparer.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace System.Collections.Immutable +namespace System.Collections.Frozen { /// /// A comparer for ordinal string comparisons. @@ -14,6 +14,6 @@ namespace System.Collections.Immutable internal sealed class FullStringComparer : StringComparerBase { public override bool Equals(string? x, string? y) => string.Equals(x, y); - public override int GetHashCode(string s) => Hashing.GetHashCode(s.AsSpan()); + public override int GetHashCode(string s) => GetHashCodeOrdinal(s.AsSpan()); } } diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/Hashing.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/Hashing.cs deleted file mode 100644 index 38961216c9b35..0000000000000 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/Hashing.cs +++ /dev/null @@ -1,110 +0,0 @@ -// 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.Numerics; - -namespace System.Collections.Immutable -{ - internal static class Hashing - { - public static unsafe int GetHashCode(ReadOnlySpan s) - { - uint hash1 = (5381 << 16) + 5381; - uint hash2 = hash1; - - fixed (char* c = s) - { - uint* ptr = (uint*)c; - int length = s.Length; - - while (length > 3) - { - hash1 = BitOperations.RotateLeft(hash1, 5) + hash1 ^ *ptr++; - hash2 = BitOperations.RotateLeft(hash2, 5) + hash2 ^ *ptr++; - length -= 4; - } - - char* tail = (char*)ptr; - while (length-- > 0) - { - hash2 = BitOperations.RotateLeft(hash2, 5) + hash2 ^ *tail++; - } - - return (int)(hash1 + (hash2 * 1_566_083_941)); - } - } - - // useful if the string only contains ASCII characterss - public static unsafe int GetCaseInsensitiveAsciiHashCode(ReadOnlySpan s) - { - uint hash1 = (5381 << 16) + 5381; - uint hash2 = hash1; - - fixed (char* src = s) - { - uint* ptr = (uint*)src; - int length = s.Length; - - // We "normalize to lowercase" every char by ORing with 0x0020. This casts - // a very wide net because it will change, e.g., '^' to '~'. But that should - // be ok because we expect this to be very rare in practice. - const uint NormalizeToLowercase = 0x0020_0020u; // valid both for big-endian and for little-endian - - while (length > 3) - { - hash1 = BitOperations.RotateLeft(hash1, 5) + hash1 ^ (*ptr++ | NormalizeToLowercase); - hash2 = BitOperations.RotateLeft(hash2, 5) + hash2 ^ (*ptr++ | NormalizeToLowercase); - length -= 4; - } - - char* tail = (char*)ptr; - while (length-- > 0) - { - hash2 = BitOperations.RotateLeft(hash2, 5) + hash2 ^ (*tail++ | NormalizeToLowercase); - } - } - - return (int)(hash1 + (hash2 * 1_566_083_941)); - } - - public static unsafe int GetCaseInsensitiveHashCode(ReadOnlySpan s) - { - int length = s.Length; - - char[]? rentedArray = null; - Span scratch = length <= 256 ? - stackalloc char[256] : - (rentedArray = ArrayPool.Shared.Rent(length)); - - length = s.ToUpperInvariant(scratch); // WARNING: this really should be ToUpperOrdinal, but .NET doesn't offer this as a primitive - - uint hash1 = (5381 << 16) + 5381; - uint hash2 = hash1; - - fixed (char* src = scratch) - { - uint* ptr = (uint*)src; - while (length > 3) - { - hash1 = (BitOperations.RotateLeft(hash1, 5) + hash1) ^ *ptr++; - hash2 = (BitOperations.RotateLeft(hash2, 5) + hash2) ^ *ptr++; - length -= 4; - } - - char* tail = (char*)ptr; - while (length-- > 0) - { - hash2 = BitOperations.RotateLeft(hash2, 5) + hash2 ^ *tail++; - } - } - - if (rentedArray is not null) - { - ArrayPool.Shared.Return(rentedArray); - } - - return (int)(hash1 + (hash2 * 1_566_083_941)); - } - } -} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedCaseInsensitiveAsciiSubstringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedCaseInsensitiveAsciiSubstringComparer.cs index 13badd2545466..3fa6ba9d9ae99 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedCaseInsensitiveAsciiSubstringComparer.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedCaseInsensitiveAsciiSubstringComparer.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace System.Collections.Immutable +namespace System.Collections.Frozen { /// /// A comparer that operates over a portion of the input strings. @@ -17,7 +17,6 @@ internal sealed class LeftJustifiedCaseInsensitiveAsciiSubstringComparer : Subst { public override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y); public override bool EqualsPartial(string? x, string? y) => x.AsSpan(Index, Count).Equals(y.AsSpan(Index, Count), StringComparison.OrdinalIgnoreCase); - public override int GetHashCode(string s) => Hashing.GetCaseInsensitiveAsciiHashCode(s.AsSpan(Index, Count)); - public override bool CaseInsensitive => true; + public override int GetHashCode(string s) => GetHashCodeOrdinalIgnoreCaseAscii(s.AsSpan(Index, Count)); } } diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedCaseInsensitiveSubstringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedCaseInsensitiveSubstringComparer.cs index c39f84e95642c..6faa142f133d6 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedCaseInsensitiveSubstringComparer.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedCaseInsensitiveSubstringComparer.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace System.Collections.Immutable +namespace System.Collections.Frozen { /// /// A comparer that operates over a portion of the input strings. @@ -17,7 +17,6 @@ internal sealed class LeftJustifiedCaseInsensitiveSubstringComparer : SubstringC { public override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y); public override bool EqualsPartial(string? x, string? y) => x.AsSpan(Index, Count).Equals(y.AsSpan(Index, Count), StringComparison.OrdinalIgnoreCase); - public override int GetHashCode(string s) => Hashing.GetCaseInsensitiveHashCode(s.AsSpan(Index, Count)); - public override bool CaseInsensitive => true; + public override int GetHashCode(string s) => GetHashCodeOrdinalIgnoreCase(s.AsSpan(Index, Count)); } } diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedSingleCharComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedSingleCharComparer.cs index 2a1b3a5d22b2d..43aca9823b39c 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedSingleCharComparer.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedSingleCharComparer.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace System.Collections.Immutable +namespace System.Collections.Frozen { /// /// A comparer that operates over a single char of the input strings. diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedSubstringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedSubstringComparer.cs index 1175b83769893..160c524a3f017 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedSubstringComparer.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedSubstringComparer.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace System.Collections.Immutable +namespace System.Collections.Frozen { /// /// A comparer that operates over a portion of the input strings. @@ -16,7 +16,7 @@ namespace System.Collections.Immutable internal sealed class LeftJustifiedSubstringComparer : SubstringComparerBase { public override bool Equals(string? x, string? y) => string.Equals(x, y); - public override bool EqualsPartial(string? x, string? y) => x.AsSpan(Index, Count).Equals(y.AsSpan(Index, Count), StringComparison.Ordinal); - public override int GetHashCode(string s) => Hashing.GetHashCode(s.AsSpan(Index, Count)); + public override bool EqualsPartial(string? x, string? y) => x.AsSpan(Index, Count).SequenceEqual(y.AsSpan(Index, Count)); + public override int GetHashCode(string s) => GetHashCodeOrdinal(s.AsSpan(Index, Count)); } } diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedCaseInsensitiveAsciiSubstringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedCaseInsensitiveAsciiSubstringComparer.cs index 725cbd2fd71ac..cb6f72c757121 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedCaseInsensitiveAsciiSubstringComparer.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedCaseInsensitiveAsciiSubstringComparer.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace System.Collections.Immutable +namespace System.Collections.Frozen { /// /// A comparer that operates over a portion of the input strings. @@ -17,7 +17,6 @@ internal sealed class RightJustifiedCaseInsensitiveAsciiSubstringComparer : Subs { public override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y); public override bool EqualsPartial(string? x, string? y) => x.AsSpan(x!.Length + Index, Count).Equals(y.AsSpan(y!.Length + Index, Count), StringComparison.OrdinalIgnoreCase); - public override int GetHashCode(string s) => Hashing.GetCaseInsensitiveAsciiHashCode(s.AsSpan(s.Length + Index, Count)); - public override bool CaseInsensitive => true; + public override int GetHashCode(string s) => GetHashCodeOrdinalIgnoreCaseAscii(s.AsSpan(s.Length + Index, Count)); } } diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedCaseInsensitiveSubstringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedCaseInsensitiveSubstringComparer.cs index 623d5920928f6..37578f6eddaf3 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedCaseInsensitiveSubstringComparer.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedCaseInsensitiveSubstringComparer.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace System.Collections.Immutable +namespace System.Collections.Frozen { /// /// A comparer that operates over a portion of the input strings. @@ -17,7 +17,6 @@ internal sealed class RightJustifiedCaseInsensitiveSubstringComparer : Substring { public override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y); public override bool EqualsPartial(string? x, string? y) => x.AsSpan(x!.Length + Index, Count).Equals(y.AsSpan(y!.Length + Index, Count), StringComparison.OrdinalIgnoreCase); - public override int GetHashCode(string s) => Hashing.GetCaseInsensitiveHashCode(s.AsSpan(s.Length + Index, Count)); - public override bool CaseInsensitive => true; + public override int GetHashCode(string s) => GetHashCodeOrdinalIgnoreCase(s.AsSpan(s.Length + Index, Count)); } } diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedSingleCharComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedSingleCharComparer.cs index a0c25be285f74..2508342b4db32 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedSingleCharComparer.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedSingleCharComparer.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace System.Collections.Immutable +namespace System.Collections.Frozen { /// /// A comparer that operates over a single character of the input strings. diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedSubstringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedSubstringComparer.cs index 2c140e709d6fb..2c1367affb251 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedSubstringComparer.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedSubstringComparer.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace System.Collections.Immutable +namespace System.Collections.Frozen { /// /// A comparer that operates over a portion of the input strings. @@ -16,7 +16,7 @@ namespace System.Collections.Immutable internal sealed class RightJustifiedSubstringComparer : SubstringComparerBase { public override bool Equals(string? x, string? y) => string.Equals(x, y); - public override bool EqualsPartial(string? x, string? y) => x.AsSpan(x!.Length + Index, Count).Equals(y.AsSpan(y!.Length + Index, Count), StringComparison.Ordinal); - public override int GetHashCode(string s) => Hashing.GetHashCode(s.AsSpan(s.Length + Index, Count)); + public override bool EqualsPartial(string? x, string? y) => x.AsSpan(x!.Length + Index, Count).SequenceEqual(y.AsSpan(y!.Length + Index, Count)); + public override int GetHashCode(string s) => GetHashCodeOrdinal(s.AsSpan(s.Length + Index, Count)); } } diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/StringComparerBase.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/StringComparerBase.cs index 28ba04fb6fd83..da80dc8c125fc 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/StringComparerBase.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/StringComparerBase.cs @@ -1,22 +1,118 @@ // 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.Runtime.CompilerServices; +using System.Numerics; +using System.Runtime.InteropServices; -namespace System.Collections.Immutable +namespace System.Collections.Frozen { // We define this rather than using IEqualityComparer, since virtual dispatch is faster than interface dispatch - internal abstract class StringComparerBase : IEqualityComparer + internal abstract class StringComparerBase : EqualityComparer { - public int MinLength; - public int MaxLength; + // TODO https://github.com/dotnet/runtime/issues/77679: + // Replace these once non-randomized implementations are available. - public abstract bool Equals(string? x, string? y); - public abstract int GetHashCode(string s); - public virtual bool CaseInsensitive => false; + protected static unsafe int GetHashCodeOrdinal(ReadOnlySpan s) + { + int length = s.Length; + fixed (char* src = &MemoryMarshal.GetReference(s)) + { + uint hash1 = (5381 << 16) + 5381; + uint hash2 = hash1; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TrivialReject(string s) => s.Length < MinLength || s.Length > MaxLength; + uint* ptrUInt32 = (uint*)src; + while (length > 3) + { + hash1 = BitOperations.RotateLeft(hash1, 5) + hash1 ^ ptrUInt32[0]; + hash2 = BitOperations.RotateLeft(hash2, 5) + hash2 ^ ptrUInt32[1]; + ptrUInt32 += 2; + length -= 4; + } + + char* ptrChar = (char*)ptrUInt32; + while (length-- > 0) + { + hash2 = BitOperations.RotateLeft(hash2, 5) + hash2 ^ *ptrChar++; + } + + return (int)(hash1 + (hash2 * 1_566_083_941)); + } + } + + // useful if the string only contains ASCII characterss + protected static unsafe int GetHashCodeOrdinalIgnoreCaseAscii(ReadOnlySpan s) + { + int length = s.Length; + fixed (char* src = &MemoryMarshal.GetReference(s)) + { + uint hash1 = (5381 << 16) + 5381; + uint hash2 = hash1; + + // We "normalize to lowercase" every char by ORing with 0x0020. This casts + // a very wide net because it will change, e.g., '^' to '~'. But that should + // be ok because we expect this to be very rare in practice. + const uint NormalizeToLowercase = 0x0020_0020u; // valid both for big-endian and for little-endian + + uint* ptrUInt32 = (uint*)src; + while (length > 3) + { + hash1 = BitOperations.RotateLeft(hash1, 5) + hash1 ^ (ptrUInt32[0] | NormalizeToLowercase); + hash2 = BitOperations.RotateLeft(hash2, 5) + hash2 ^ (ptrUInt32[1] | NormalizeToLowercase); + ptrUInt32 += 2; + length -= 4; + } + + char* ptrChar = (char*)ptrUInt32; + while (length-- > 0) + { + hash2 = BitOperations.RotateLeft(hash2, 5) + hash2 ^ (*ptrChar | NormalizeToLowercase); + ptrChar++; + } + + return (int)(hash1 + (hash2 * 1_566_083_941)); + } + } + + protected static unsafe int GetHashCodeOrdinalIgnoreCase(ReadOnlySpan s) + { + int length = s.Length; + + char[]? rentedArray = null; + Span scratch = length <= 256 ? + stackalloc char[256] : + (rentedArray = ArrayPool.Shared.Rent(length)); + + length = s.ToUpperInvariant(scratch); // NOTE: this really should be the (non-existent) ToUpperOrdinal + + uint hash1 = (5381 << 16) + 5381; + uint hash2 = hash1; + + fixed (char* src = &MemoryMarshal.GetReference(scratch)) + { + uint* ptrUInt32 = (uint*)src; + while (length > 3) + { + hash1 = (BitOperations.RotateLeft(hash1, 5) + hash1) ^ ptrUInt32[0]; + hash2 = (BitOperations.RotateLeft(hash2, 5) + hash2) ^ ptrUInt32[1]; + ptrUInt32 += 2; + length -= 4; + } + + char* ptrChar = (char*)ptrUInt32; + while (length-- > 0) + { + hash2 = BitOperations.RotateLeft(hash2, 5) + hash2 ^ *ptrChar++; + } + } + + if (rentedArray is not null) + { + ArrayPool.Shared.Return(rentedArray); + } + + return (int)(hash1 + (hash2 * 1_566_083_941)); + } } } diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/SubstringComparerBase.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/SubstringComparerBase.cs index 04ab86ed304bd..4f382373dde66 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/SubstringComparerBase.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/SubstringComparerBase.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace System.Collections.Immutable +namespace System.Collections.Frozen { internal abstract class SubstringComparerBase : StringComparerBase { 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 new file mode 100644 index 0000000000000..5bf563de58ed6 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ValueTypeDefaultComparerFrozenDictionary.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.Collections.Frozen +{ + /// Provides a frozen dictionary optimized for value type keys using the default comparer. + /// The type of keys in the dictionary. + /// The type of values in the dictionary. + internal sealed class ValueTypeDefaultComparerFrozenDictionary : KeysAndValuesFrozenDictionary, IDictionary + where TKey : notnull + { + internal ValueTypeDefaultComparerFrozenDictionary(Dictionary source) : + base(source, EqualityComparer.Default) + { + Debug.Assert(typeof(TKey).IsValueType); + } + + /// + private protected override ref readonly TValue GetValueRefOrNullRefCore(TKey key) + { + int hashCode = EqualityComparer.Default.GetHashCode(key); + _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); + + while (index <= endIndex) + { + if (hashCode == _hashTable.HashCodes[index]) + { + if (EqualityComparer.Default.Equals(key, _keys[index])) + { + return ref _values[index]; + } + } + + index++; + } + + return ref Unsafe.NullRef(); + } + } +} 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 new file mode 100644 index 0000000000000..6eca1831646a0 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ValueTypeDefaultComparerFrozenSet.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Collections.Frozen +{ + /// Provides a frozen set optimized for value types using the default comparer. + /// The type of values in the dictionary. + internal sealed class ValueTypeDefaultComparerFrozenSet : ItemsFrozenSet.GSW> + { + internal ValueTypeDefaultComparerFrozenSet(HashSet source) : + base(source, EqualityComparer.Default) + { + Debug.Assert(typeof(T).IsValueType); + } + + /// + private protected override int FindItemIndex(T item) + { + int hashCode = EqualityComparer.Default.GetHashCode(item!); + _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); + + while (index <= endIndex) + { + if (hashCode == _hashTable.HashCodes[index]) + { + if (EqualityComparer.Default.Equals(item, _items[index])) + { + return index; + } + } + + index++; + } + + return -1; + } + + internal struct GSW : IGenericSpecializedWrapper + { + private ValueTypeDefaultComparerFrozenSet _set; + public void Store(FrozenSet set) => _set = (ValueTypeDefaultComparerFrozenSet)set; + + public int Count => _set.Count; + public IEqualityComparer Comparer => _set.Comparer; + public int FindItemIndex(T item) => _set.FindItemIndex(item); + public Enumerator GetEnumerator() => _set.GetEnumerator(); + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableEnumerableDebuggerProxy.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableEnumerableDebuggerProxy.cs index 17206683bbd23..3df4cd8e08b4a 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableEnumerableDebuggerProxy.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableEnumerableDebuggerProxy.cs @@ -18,7 +18,7 @@ internal sealed class ImmutableDictionaryDebuggerProxy : Immutable /// Initializes a new instance of the class. /// /// The enumerable to show in the debugger. - public ImmutableDictionaryDebuggerProxy(IImmutableDictionary dictionary) + public ImmutableDictionaryDebuggerProxy(IReadOnlyDictionary dictionary) : base(enumerable: dictionary) { } diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/ThrowHelper.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/ThrowHelper.cs index 0e9dc1e03fd8d..faf71460a5465 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/ThrowHelper.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/ThrowHelper.cs @@ -1,24 +1,36 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Runtime.Serialization; +using System.Runtime.CompilerServices; namespace System.Collections { internal static class ThrowHelper { - public static void IfBufferTooSmall(int actual, int required) + public static void ThrowIfNull(object arg, [CallerArgumentExpression("arg")] string? paramName = null) { - if (actual < required) + if (arg is null) { - ThrowDestinationArrayTooSmall(); + ThrowArgumentNullException(paramName); } } [DoesNotReturn] - public static void ThrowDestinationArrayTooSmall() => - throw new ArgumentException(SR.CapacityMustBeGreaterThanOrEqualToCount); + public static void ThrowIfDestinationTooSmall() => + throw new ArgumentException(SR.CapacityMustBeGreaterThanOrEqualToCount, "destination"); + + [DoesNotReturn] + public static void ThrowArgumentNullException(string? paramName) => + throw new ArgumentNullException(paramName); + + [DoesNotReturn] + public static void ThrowKeyNotFoundException() => + throw new KeyNotFoundException(); + + [DoesNotReturn] + public static void ThrowInvalidOperationException() => + throw new InvalidOperationException(); } } diff --git a/src/libraries/System.Collections.Immutable/src/System/Polyfills.cs b/src/libraries/System.Collections.Immutable/src/System/Polyfills.cs new file mode 100644 index 0000000000000..77f304f46fdcd --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Polyfills.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace System.Collections.Generic +{ +#if !NETCOREAPP2_0_OR_GREATER + internal static class KeyValuePairExtensions + { + [EditorBrowsable(EditorBrowsableState.Never)] + public static void Deconstruct(this KeyValuePair source, out TKey key, out TValue value) + { + key = source.Key; + value = source.Value; + } + } +#endif + +#if !NET5_0_OR_GREATER + internal interface IReadOnlySet : IReadOnlyCollection + { + bool Contains(T item); + bool IsProperSubsetOf(IEnumerable other); + bool IsProperSupersetOf(IEnumerable other); + bool IsSubsetOf(IEnumerable other); + bool IsSupersetOf(IEnumerable other); + bool Overlaps(IEnumerable other); + bool SetEquals(IEnumerable other); + } +#endif +} + +namespace System.Numerics +{ +#if !NETCOREAPP3_0_OR_GREATER + internal static class BitOperations + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint RotateLeft(uint value, int offset) => (value << offset) | (value >> (32 - offset)); + } +#endif +} + +namespace System.Runtime.CompilerServices +{ +#if !NETCOREAPP3_0_OR_GREATER + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] + internal sealed class CallerArgumentExpressionAttribute : Attribute + { + public CallerArgumentExpressionAttribute(string parameterName) => ParameterName = parameterName; + + public string ParameterName { get; } + } +#endif +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Stubs.cs b/src/libraries/System.Collections.Immutable/src/System/Stubs.cs deleted file mode 100644 index 14eb362af33b3..0000000000000 --- a/src/libraries/System.Collections.Immutable/src/System/Stubs.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.ComponentModel; -using System.Runtime.CompilerServices; - -namespace System.Numerics -{ -#if !NETCOREAPP3_0_OR_GREATER - internal static class BitOperations - { - /// - /// Rotates the specified value left by the specified number of bits. - /// Similar in behavior to the x86 instruction ROL. - /// - /// The value to rotate. - /// The number of bits to rotate by. - /// Any value outside the range [0..31] is treated as congruent mod 32. - /// The rotated value. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static uint RotateLeft(uint value, int offset) => - (value << offset) | (value >> (32 - offset)); - } -#endif -} - -namespace System.Collections.Generic -{ - internal static class KeyValuePairExtensions - { - [EditorBrowsable(EditorBrowsableState.Never)] - public static void Deconstruct(this KeyValuePair source, out TKey key, out TValue value) - { - key = source.Key; - value = source.Value; - } - } - -#if !NET5_0_OR_GREATER - /// - /// Provides a readonly abstraction of a set. - /// - /// The type of elements in the set. - public interface IReadOnlySet : IReadOnlyCollection // TODO: fix this - { - /// - /// Determines if the set contains a specific item - /// - /// The item to check if the set contains. - /// if found; otherwise . - bool Contains(T item); - - /// - /// Determines whether the current set is a proper (strict) subset of a specified collection. - /// - /// The collection to compare to the current set. - /// if the current set is a proper subset of other; otherwise . - /// other is . - bool IsProperSubsetOf(IEnumerable other); - - /// - /// Determines whether the current set is a proper (strict) superset of a specified collection. - /// - /// The collection to compare to the current set. - /// if the collection is a proper superset of other; otherwise . - /// other is . - bool IsProperSupersetOf(IEnumerable other); - - /// - /// Determine whether the current set is a subset of a specified collection. - /// - /// The collection to compare to the current set. - /// if the current set is a subset of other; otherwise . - /// other is . - bool IsSubsetOf(IEnumerable other); - - /// - /// Determine whether the current set is a super set of a specified collection. - /// - /// The collection to compare to the current set - /// if the current set is a subset of other; otherwise . - /// other is . - bool IsSupersetOf(IEnumerable other); - - /// - /// Determines whether the current set overlaps with the specified collection. - /// - /// The collection to compare to the current set. - /// if the current set and other share at least one common element; otherwise, . - /// other is . - bool Overlaps(IEnumerable other); - - /// - /// Determines whether the current set and the specified collection contain the same elements. - /// - /// The collection to compare to the current set. - /// if the current set is equal to other; otherwise, . - /// other is . - bool SetEquals(IEnumerable other); - } -#endif -} diff --git a/src/libraries/System.Collections.Immutable/src/Validation/Requires.cs b/src/libraries/System.Collections.Immutable/src/Validation/Requires.cs index fbc80dedcb389..77a034a739141 100644 --- a/src/libraries/System.Collections.Immutable/src/Validation/Requires.cs +++ b/src/libraries/System.Collections.Immutable/src/Validation/Requires.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; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -21,7 +20,7 @@ internal static class Requires /// The name of the parameter to include in any thrown exception. /// Thrown if is null [DebuggerStepThrough] - public static void NotNull([ValidatedNotNull][NotNull]T value, string? parameterName) + public static void NotNull([NotNull]T value, string? parameterName) where T : class // ensures value-types aren't passed to a null checking method { if (value == null) @@ -39,7 +38,7 @@ public static void NotNull([ValidatedNotNull][NotNull]T value, string? parame /// The value of the parameter. /// Thrown if is null [DebuggerStepThrough] - public static T NotNullPassthrough([ValidatedNotNull][NotNull]T value, string? parameterName) + public static T NotNullPassthrough([NotNull]T value, string? parameterName) where T : class // ensures value-types aren't passed to a null checking method { NotNull(value, parameterName); @@ -58,7 +57,7 @@ public static T NotNullPassthrough([ValidatedNotNull][NotNull]T value, string /// may or may not be a class, but certainly cannot be null. /// [DebuggerStepThrough] - public static void NotNullAllowStructs([ValidatedNotNull][NotNull]T value, string? parameterName) + public static void NotNullAllowStructs([NotNull]T value, string? parameterName) { if (null == value) { @@ -72,7 +71,7 @@ public static void NotNullAllowStructs([ValidatedNotNull][NotNull]T value, st /// The name of the parameter that was null. [DoesNotReturn] [DebuggerStepThrough] - private static void FailArgumentNullException(string? parameterName) + public static void FailArgumentNullException(string? parameterName) { // Separating out this throwing operation helps with inlining of the caller throw new ArgumentNullException(parameterName); diff --git a/src/libraries/System.Collections.Immutable/src/Validation/ValidatedNotNullAttribute.cs b/src/libraries/System.Collections.Immutable/src/Validation/ValidatedNotNullAttribute.cs deleted file mode 100644 index a9b26011bc47a..0000000000000 --- a/src/libraries/System.Collections.Immutable/src/Validation/ValidatedNotNullAttribute.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; - -namespace System.Collections.Immutable -{ - /// - /// Indicates to Code Analysis that a method validates a particular parameter. - /// - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] - internal sealed class ValidatedNotNullAttribute : Attribute - { - } -} diff --git a/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenDictionaryTest.cs b/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenDictionaryTest.cs new file mode 100644 index 0000000000000..aaa24662f09a4 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenDictionaryTest.cs @@ -0,0 +1,480 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Tests; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; +using Xunit; + +namespace System.Collections.Frozen.Tests +{ + public abstract class FrozenDictionary_Generic_Tests : IDictionary_Generic_Tests + { + protected override bool IsReadOnly => true; + protected override bool AddRemoveClear_ThrowsNotSupported => true; + protected override bool Enumerator_Current_UndefinedOperation_Throws => true; + protected override Type ICollection_Generic_CopyTo_IndexLargerThanArrayCount_ThrowType => typeof(ArgumentOutOfRangeException); + + protected override IDictionary GenericIDictionaryFactory(int count) + { + var d = new Dictionary(); + for (int i = 0; i < count; i++) + { + d.Add(CreateTKey(i), CreateTValue(i)); + } + return d.ToFrozenDictionary(GetKeyIEqualityComparer()); + } + + protected override IDictionary GenericIDictionaryFactory() => Enumerable.Empty>().ToFrozenDictionary(); + + protected override IDictionary GenericIDictionaryFactory(IEqualityComparer comparer) => Enumerable.Empty>().ToFrozenDictionary(comparer); + + protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) => new List(); + + protected override bool ResetImplemented => true; + protected override bool IDictionary_Generic_Keys_Values_Enumeration_ResetImplemented => true; + + protected override EnumerableOrder Order => EnumerableOrder.Unspecified; + + [Theory] + [InlineData(100_000)] + public void CreateVeryLargeDictionary_Success(int largeCount) + { + GenericIDictionaryFactory(largeCount); + } + + [Fact] + public void NullSource_ThrowsException() + { + AssertExtensions.Throws("source", () => ((Dictionary)null).ToFrozenDictionary()); + AssertExtensions.Throws("source", () => ((Dictionary)null).ToFrozenDictionary(null)); + AssertExtensions.Throws("source", () => ((Dictionary)null).ToFrozenDictionary(EqualityComparer.Default)); + + AssertExtensions.Throws("keySelector", () => Enumerable.Empty().ToFrozenDictionary((Func)null)); + AssertExtensions.Throws("keySelector", () => Enumerable.Empty().ToFrozenDictionary((Func)null, EqualityComparer.Default)); + AssertExtensions.Throws("keySelector", () => Enumerable.Empty().ToFrozenDictionary((Func)null, (Func)null, EqualityComparer.Default)); + + AssertExtensions.Throws("elementSelector", () => Enumerable.Empty().ToFrozenDictionary(i => i, (Func)null)); + AssertExtensions.Throws("elementSelector", () => Enumerable.Empty().ToFrozenDictionary(i => i, (Func)null, EqualityComparer.Default)); + } + + [Fact] + public void EmptySource_ProducedFrozenDictionaryEmpty() + { + Assert.Same(FrozenDictionary.Empty, new Dictionary().ToFrozenDictionary()); + Assert.Same(FrozenDictionary.Empty, Enumerable.Empty>().ToFrozenDictionary()); + Assert.Same(FrozenDictionary.Empty, Array.Empty>().ToFrozenDictionary()); + Assert.Same(FrozenDictionary.Empty, new List>().ToFrozenDictionary()); + + foreach (IEqualityComparer comparer in new IEqualityComparer[] { null, EqualityComparer.Default, NonDefaultEqualityComparer.Instance }) + { + Assert.Same(FrozenDictionary.Empty, new Dictionary().ToFrozenDictionary(comparer)); + Assert.Same(FrozenDictionary.Empty, Enumerable.Empty>().ToFrozenDictionary(comparer)); + Assert.Same(FrozenDictionary.Empty, Array.Empty>().ToFrozenDictionary(comparer)); + Assert.Same(FrozenDictionary.Empty, new List>().ToFrozenDictionary(comparer)); + } + } + + [Fact] + public void EmptyFrozenDictionary_Idempotent() + { + FrozenDictionary empty = FrozenDictionary.Empty; + + Assert.NotNull(empty); + Assert.Same(empty, FrozenDictionary.Empty); + } + + [Fact] + public void EmptyFrozenDictionary_OperationsAreNops() + { + FrozenDictionary empty = FrozenDictionary.Empty; + + Assert.Same(EqualityComparer.Default, empty.Comparer); + Assert.Equal(0, empty.Count); + Assert.Empty(empty.Keys); + Assert.Empty(empty.Values); + + TKey key = CreateTKey(0); + Assert.False(empty.ContainsKey(key)); + Assert.False(empty.TryGetValue(key, out TValue value)); + Assert.Equal(default, value); + Assert.True(Unsafe.IsNullRef(ref Unsafe.AsRef(in empty.GetValueRefOrNullRef(key)))); + Assert.Throws(() => empty[key]); + + empty.CopyTo(Span>.Empty); + KeyValuePair[] array = new KeyValuePair[1]; + empty.CopyTo(array); + Assert.Equal(default, array[0]); + + int count = 0; + foreach (KeyValuePair pair in empty) + { + count++; + } + Assert.Equal(0, count); + } + + [Fact] + public void FrozenDictionary_ToFrozenDictionary_Idempotent() + { + foreach (IEqualityComparer comparer in new IEqualityComparer[] { null, EqualityComparer.Default, NonDefaultEqualityComparer.Instance }) + { + Assert.Same(FrozenDictionary.Empty, FrozenDictionary.Empty.ToFrozenDictionary(comparer)); + } + + FrozenDictionary frozen = new Dictionary() { { CreateTKey(0), CreateTValue(0) } }.ToFrozenDictionary(); + Assert.Same(frozen, frozen.ToFrozenDictionary()); + Assert.NotSame(frozen, frozen.ToFrozenDictionary(NonDefaultEqualityComparer.Instance)); + } + + public static IEnumerable LookupItems_AllItemsFoundAsExpected_MemberData() + { + foreach (int size in new[] { 1, 2, 10, 999, 1024 }) + { + foreach (IEqualityComparer comparer in new IEqualityComparer[] { null, EqualityComparer.Default, NonDefaultEqualityComparer.Instance }) + { + foreach (bool specifySameComparer in new[] { false, true }) + { + yield return new object[] { size, comparer, specifySameComparer }; + } + } + } + } + + [Fact] + public void ToFrozenDictionary_KeySelector_ResultsAreUsed() + { + TKey[] keys = Enumerable.Range(0, 10).Select(CreateTKey).ToArray(); + + FrozenDictionary frozen = Enumerable.Range(0, 10).ToFrozenDictionary(i => keys[i], NonDefaultEqualityComparer.Instance); + Assert.Same(NonDefaultEqualityComparer.Instance, frozen.Comparer); + + for (int i = 0; i < 10; i++) + { + Assert.Equal(i, frozen[keys[i]]); + } + } + + [Fact] + public void ToFrozenDictionary_KeySelectorAndValueSelector_ResultsAreUsed() + { + TKey[] keys = Enumerable.Range(0, 10).Select(CreateTKey).ToArray(); + TValue[] values = Enumerable.Range(0, 10).Select(CreateTValue).ToArray(); + + FrozenDictionary frozen = Enumerable.Range(0, 10).ToFrozenDictionary(i => keys[i], i => values[i], NonDefaultEqualityComparer.Instance); + Assert.Same(NonDefaultEqualityComparer.Instance, frozen.Comparer); + + for (int i = 0; i < 10; i++) + { + Assert.Equal(values[i], frozen[keys[i]]); + } + } + + [Theory] + [MemberData(nameof(LookupItems_AllItemsFoundAsExpected_MemberData))] + public void LookupItems_AllItemsFoundAsExpected(int size, IEqualityComparer comparer, bool specifySameComparer) + { + Dictionary original = + Enumerable.Range(0, size) + .Select(i => new KeyValuePair(CreateTKey(i), CreateTValue(i))) + .ToDictionary(p => p.Key, p => p.Value, comparer); + KeyValuePair[] originalPairs = original.ToArray(); + + FrozenDictionary frozen = specifySameComparer ? + original.ToFrozenDictionary(comparer) : + original.ToFrozenDictionary(); + + // Make sure creating the frozen dictionary didn't alter the original + Assert.Equal(originalPairs.Length, original.Count); + Assert.All(originalPairs, p => Assert.Equal(p.Value, original[p.Key])); + + // Make sure the frozen dictionary matches the original + Assert.Equal(original.Count, frozen.Count); + Assert.Equal(new HashSet>(original), new HashSet>(frozen)); + Assert.All(originalPairs, p => Assert.True(frozen.ContainsKey(p.Key))); + Assert.All(originalPairs, p => Assert.Equal(p.Value, frozen[p.Key])); + Assert.All(originalPairs, p => Assert.Equal(p.Value, frozen.GetValueRefOrNullRef(p.Key))); + if (specifySameComparer || + comparer is null || + comparer == EqualityComparer.Default) + { + Assert.Equal(original.Comparer, frozen.Comparer); + } + + // Generate additional items and ensure they match iff the original matches. + for (int i = size; i < size + 100; i++) + { + TKey key = CreateTKey(i); + if (original.ContainsKey(key)) + { + Assert.True(frozen.ContainsKey(key)); + } + else + { + Assert.Throws(() => frozen[key]); + Assert.False(frozen.TryGetValue(key, out TValue value)); + Assert.Equal(default, value); + Assert.True(Unsafe.IsNullRef(ref Unsafe.AsRef(in frozen.GetValueRefOrNullRef(key)))); + } + } + } + + [Fact] + public void MultipleValuesSameKey_LastInSourceWins() + { + TKey[] keys = Enumerable.Range(0, 2).Select(CreateTKey).ToArray(); + TValue[] values = Enumerable.Range(0, 10).Select(CreateTValue).ToArray(); + + foreach (bool reverse in new[] { false, true }) + { + IEnumerable> source = + from key in keys + from value in values + select new KeyValuePair(key, value); + + if (reverse) + { + source = source.Reverse(); + } + + FrozenDictionary frozen = source.ToFrozenDictionary(GetKeyIEqualityComparer()); + + Assert.Equal(values[reverse ? 0 : values.Length - 1], frozen[keys[0]]); + Assert.Equal(values[reverse ? 0 : values.Length - 1], frozen[keys[1]]); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IReadOnlyDictionary_Generic_Keys_ContainsAllCorrectKeys(int count) + { + IDictionary dictionary = GenericIDictionaryFactory(count); + IEnumerable expected = dictionary.Select((pair) => pair.Key); + + IReadOnlyDictionary rod = (IReadOnlyDictionary)dictionary; + Assert.True(expected.SequenceEqual(rod.Keys)); + Assert.All(expected, k => rod.ContainsKey(k)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IReadOnlyDictionary_Generic_Values_ContainsAllCorrectValues(int count) + { + IDictionary dictionary = GenericIDictionaryFactory(count); + IEnumerable expected = dictionary.Select((pair) => pair.Value); + + IReadOnlyDictionary rod = (IReadOnlyDictionary)dictionary; + Assert.True(expected.SequenceEqual(rod.Values)); + + foreach (KeyValuePair pair in dictionary) + { + Assert.Equal(dictionary[pair.Key], rod[pair.Key]); + } + + Assert.All(dictionary, pair => + { + Assert.True(rod.TryGetValue(pair.Key, out TValue value)); + Assert.Equal(pair.Value, value); + }); + } + } + + public abstract class FrozenDictionary_Generic_Tests_string_string : FrozenDictionary_Generic_Tests + { + protected override KeyValuePair CreateT(int seed) + { + return new KeyValuePair(CreateTKey(seed), CreateTKey(seed + 500)); + } + + protected override string CreateTKey(int seed) + { + int stringLength = seed % 10 + 5; + Random rand = new Random(seed); + byte[] bytes1 = new byte[stringLength]; + rand.NextBytes(bytes1); + return Convert.ToBase64String(bytes1); + } + + protected override string CreateTValue(int seed) => CreateTKey(seed); + } + + public class FrozenDictionary_Generic_Tests_string_string_Default : FrozenDictionary_Generic_Tests_string_string + { + public override IEqualityComparer GetKeyIEqualityComparer() => EqualityComparer.Default; + } + + public class FrozenDictionary_Generic_Tests_string_string_Ordinal : FrozenDictionary_Generic_Tests_string_string + { + public override IEqualityComparer GetKeyIEqualityComparer() => StringComparer.Ordinal; + } + + public class FrozenDictionary_Generic_Tests_string_string_OrdinalIgnoreCase : FrozenDictionary_Generic_Tests_string_string + { + public override IEqualityComparer GetKeyIEqualityComparer() => StringComparer.OrdinalIgnoreCase; + } + + public class FrozenDictionary_Generic_Tests_string_string_NonDefault : FrozenDictionary_Generic_Tests_string_string + { + public override IEqualityComparer GetKeyIEqualityComparer() => NonDefaultEqualityComparer.Instance; + } + + public class FrozenDictionary_Generic_Tests_ulong_ulong : FrozenDictionary_Generic_Tests + { + protected override bool DefaultValueAllowed => true; + + protected override KeyValuePair CreateT(int seed) + { + ulong key = CreateTKey(seed); + ulong value = CreateTKey(~seed); + return new KeyValuePair(key, value); + } + + protected override ulong CreateTKey(int seed) + { + Random rand = new Random(seed); + ulong hi = unchecked((ulong)rand.Next()); + ulong lo = unchecked((ulong)rand.Next()); + return (hi << 32) | lo; + } + + protected override ulong CreateTValue(int seed) => CreateTKey(seed); + + [OuterLoop("Takes several seconds")] + [Theory] + [InlineData(8_000_000)] + public void CreateHugeDictionary_Success(int largeCount) + { + GenericIDictionaryFactory(largeCount); + } + } + + public class FrozenDictionary_Generic_Tests_int_int : FrozenDictionary_Generic_Tests + { + protected override bool DefaultValueAllowed => true; + + protected override KeyValuePair CreateT(int seed) + { + Random rand = new Random(seed); + return new KeyValuePair(rand.Next(), rand.Next()); + } + + protected override int CreateTKey(int seed) => new Random(seed).Next(); + + protected override int CreateTValue(int seed) => CreateTKey(seed); + } + + public class FrozenDictionary_Generic_Tests_SimpleClass_SimpleClass : FrozenDictionary_Generic_Tests + { + protected override KeyValuePair CreateT(int seed) + { + return new KeyValuePair(CreateTKey(seed), CreateTKey(seed + 500)); + } + + protected override SimpleClass CreateTKey(int seed) + { + int stringLength = seed % 10 + 5; + Random rand = new Random(seed); + byte[] bytes1 = new byte[stringLength]; + rand.NextBytes(bytes1); + return new SimpleClass { Value = Convert.ToBase64String(bytes1) }; + } + + protected override SimpleClass CreateTValue(int seed) => CreateTKey(seed); + } + + public class SimpleClass : IComparable + { + public string Value { get; set; } + + public int CompareTo(SimpleClass? other) => + other is null ? -1 : + Value.CompareTo(other.Value); + } + + public sealed class NonDefaultEqualityComparer : IEqualityComparer + { + public static NonDefaultEqualityComparer Instance { get; } = new(); + public bool Equals(TKey? x, TKey? y) => EqualityComparer.Default.Equals(x, y); + public int GetHashCode([DisallowNull] TKey obj) => EqualityComparer.Default.GetHashCode(obj); + } + + public class FrozenDictionary_NonGeneric_Tests : IDictionary_NonGeneric_Tests + { + protected override IDictionary NonGenericIDictionaryFactory() => FrozenDictionary.Empty; + + protected override IDictionary NonGenericIDictionaryFactory(int count) + { + var d = new Dictionary(); + for (int i = 0; i < count; i++) + { + d.Add(CreateTKey(i), CreateTValue(i)); + } + return d.ToFrozenDictionary(); + } + + protected override ICollection NonGenericICollectionFactory(int count) => NonGenericIDictionaryFactory(count); + + /// + /// Creates an object that is dependent on the seed given. The object may be either + /// a value type or a reference type, chosen based on the value of the seed. + /// + protected override object CreateTKey(int seed) + { + int stringLength = seed % 10 + 5; + Random rand = new Random(seed); + byte[] bytes = new byte[stringLength]; + rand.NextBytes(bytes); + return Convert.ToBase64String(bytes); + } + + /// + /// Creates an object that is dependent on the seed given. The object may be either + /// a value type or a reference type, chosen based on the value of the seed. + /// + protected override object CreateTValue(int seed) => CreateTKey(seed); + + protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) => new List(); + + protected override bool Enumerator_Current_UndefinedOperation_Throws => true; + + protected override bool IsReadOnly => true; + + protected override bool ResetImplemented => true; + + protected override bool IDictionary_NonGeneric_Keys_Values_Enumeration_ResetImplemented => true; + + protected override bool SupportsSerialization => false; + + protected override bool ExpectedIsFixedSize => true; + + protected override Type ICollection_NonGeneric_CopyTo_ArrayOfIncorrectReferenceType_ThrowType => typeof(ArgumentException); + + protected override Type ICollection_NonGeneric_CopyTo_IndexLargerThanArrayCount_ThrowType => typeof(ArgumentOutOfRangeException); + + [Fact] + public void ICollection_CopyTo_MultipleArrayTypesSupported() + { + FrozenDictionary frozen = new Dictionary() + { + { "hello", 123 }, + { "world", 456 } + }.ToFrozenDictionary(); + + var kvpArray = new KeyValuePair[4]; + ((ICollection)frozen).CopyTo(kvpArray, 1); + Assert.Equal(new KeyValuePair(null, 0), kvpArray[0]); + Assert.Equal(new KeyValuePair("hello", 123), kvpArray[1]); + Assert.Equal(new KeyValuePair("world", 456), kvpArray[2]); + Assert.Equal(new KeyValuePair(null, 0), kvpArray[3]); + + var deArray = new DictionaryEntry[4]; + ((ICollection)frozen).CopyTo(deArray, 2); + Assert.Equal(new DictionaryEntry(null, null), deArray[0]); + Assert.Equal(new DictionaryEntry(null, null), deArray[1]); + Assert.Equal(new DictionaryEntry("hello", 123), deArray[2]); + Assert.Equal(new DictionaryEntry("world", 456), deArray[3]); + } + } +} diff --git a/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenSetTests.cs b/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenSetTests.cs new file mode 100644 index 0000000000000..c0be30bbe8252 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenSetTests.cs @@ -0,0 +1,274 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Tests; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Xunit; + +namespace System.Collections.Frozen.Tests +{ + public abstract class FrozenSet_Generic_Tests : ISet_Generic_Tests + { + protected override bool ResetImplemented => true; + + protected override ISet GenericISetFactory() => Array.Empty().ToFrozenSet(); + + protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) => Array.Empty(); + + protected override bool IsReadOnly => true; + + protected override EnumerableOrder Order => EnumerableOrder.Unspecified; + + protected override Type ICollection_Generic_CopyTo_IndexLargerThanArrayCount_ThrowType => typeof(ArgumentOutOfRangeException); + + protected override bool Enumerator_Current_UndefinedOperation_Throws => true; + + protected override ISet GenericISetFactory(int count) + { + var s = new HashSet(); + for (int i = 0; i < count; i++) + { + s.Add(CreateT(i)); + } + return s.ToFrozenSet(GetIEqualityComparer()); + } + + [Theory] + [InlineData(100_000)] + public void CreateVeryLargeSet_Success(int largeCount) + { + GenericISetFactory(largeCount); + } + + [Fact] + public void NullSource_ThrowsException() + { + AssertExtensions.Throws("source", () => ((HashSet)null).ToFrozenSet()); + AssertExtensions.Throws("source", () => ((HashSet)null).ToFrozenSet(null)); + AssertExtensions.Throws("source", () => ((HashSet)null).ToFrozenSet(EqualityComparer.Default)); + } + + [Fact] + public void EmptySource_ProducedFrozenSetEmpty() + { + Assert.Same(FrozenSet.Empty, new List().ToFrozenSet()); + Assert.Same(FrozenSet.Empty, Enumerable.Empty().ToFrozenSet()); + Assert.Same(FrozenSet.Empty, Array.Empty().ToFrozenSet()); + Assert.Same(FrozenSet.Empty, new List().ToFrozenSet()); + + foreach (IEqualityComparer comparer in new IEqualityComparer[] { null, EqualityComparer.Default, NonDefaultEqualityComparer.Instance }) + { + Assert.Same(FrozenSet.Empty, new List().ToFrozenSet(comparer)); + Assert.Same(FrozenSet.Empty, Enumerable.Empty().ToFrozenSet(comparer)); + Assert.Same(FrozenSet.Empty, Array.Empty().ToFrozenSet(comparer)); + Assert.Same(FrozenSet.Empty, new List().ToFrozenSet(comparer)); + } + } + + [Fact] + public void EmptyFrozenSet_Idempotent() + { + FrozenSet empty = FrozenSet.Empty; + + Assert.NotNull(empty); + Assert.Same(empty, FrozenSet.Empty); + } + + [Fact] + public void EmptyFrozenSet_OperationsAreNops() + { + FrozenSet empty = FrozenSet.Empty; + + Assert.Same(EqualityComparer.Default, empty.Comparer); + Assert.Equal(0, empty.Count); + Assert.Empty(empty.Items); + + T item = CreateT(0); + Assert.False(empty.Contains(item)); + + empty.CopyTo(Span.Empty); + T[] array = new T[1]; + empty.CopyTo(array); + Assert.Equal(default, array[0]); + + int count = 0; + foreach (T value in empty) + { + count++; + } + Assert.Equal(0, count); + } + + [Fact] + public void FrozenSet_ToFrozenSet_Idempotent() + { + foreach (IEqualityComparer comparer in new IEqualityComparer[] { null, EqualityComparer.Default, NonDefaultEqualityComparer.Instance }) + { + Assert.Same(FrozenSet.Empty, FrozenSet.Empty.ToFrozenSet(comparer)); + } + + FrozenSet frozen = new HashSet() { { CreateT(0) } }.ToFrozenSet(); + Assert.Same(frozen, frozen.ToFrozenSet()); + Assert.NotSame(frozen, frozen.ToFrozenSet(NonDefaultEqualityComparer.Instance)); + } + + public static IEnumerable LookupItems_AllItemsFoundAsExpected_MemberData() + { + foreach (int size in new[] { 1, 2, 10, 999, 1024 }) + { + foreach (IEqualityComparer comparer in new IEqualityComparer[] { null, EqualityComparer.Default, NonDefaultEqualityComparer.Instance }) + { + foreach (bool specifySameComparer in new[] { false, true }) + { + yield return new object[] { size, comparer, specifySameComparer }; + } + } + } + } + + [Theory] + [MemberData(nameof(LookupItems_AllItemsFoundAsExpected_MemberData))] + public void LookupItems_AllItemsFoundAsExpected(int size, IEqualityComparer comparer, bool specifySameComparer) + { + HashSet original = new HashSet(Enumerable.Range(0, size).Select(CreateT), comparer); + T[] originalItems = original.ToArray(); + + FrozenSet frozen = specifySameComparer ? + original.ToFrozenSet(comparer) : + original.ToFrozenSet(); + + // Make sure creating the frozen dictionary didn't alter the original + Assert.Equal(originalItems.Length, original.Count); + Assert.All(originalItems, p => Assert.True(frozen.Contains(p))); + + // Make sure the frozen dictionary matches the original + Assert.Equal(original.Count, frozen.Count); + Assert.Equal(original, new HashSet(frozen)); + Assert.All(originalItems, p => Assert.True(frozen.Contains(p))); + if (specifySameComparer || + comparer is null || + comparer == EqualityComparer.Default) + { + Assert.Equal(original.Comparer, frozen.Comparer); + } + + // Generate additional items and ensure they match iff the original matches. + for (int i = size; i < size + 100; i++) + { + T item = CreateT(i); + Assert.Equal(original.Contains(item), frozen.Contains(item)); + } + } + } + + public abstract class FrozenSet_Generic_Tests_string : FrozenSet_Generic_Tests + { + protected override string CreateT(int seed) + { + int stringLength = seed % 10 + 5; + Random rand = new Random(seed); + byte[] bytes1 = new byte[stringLength]; + rand.NextBytes(bytes1); + return Convert.ToBase64String(bytes1); + } + } + + public class FrozenSet_Generic_Tests_string_Default : FrozenSet_Generic_Tests_string + { + protected override IEqualityComparer GetIEqualityComparer() => EqualityComparer.Default; + } + + public class FrozenSet_Generic_Tests_string_Ordinal : FrozenSet_Generic_Tests_string + { + protected override IEqualityComparer GetIEqualityComparer() => StringComparer.Ordinal; + } + + public class FrozenSet_Generic_Tests_string_OrdinalIgnoreCase : FrozenSet_Generic_Tests_string + { + protected override IEqualityComparer GetIEqualityComparer() => StringComparer.OrdinalIgnoreCase; + + [Fact] + public void TryGetValue_FindsExpectedResult() + { + FrozenSet frozen = new[] { "abc" }.ToFrozenSet(StringComparer.OrdinalIgnoreCase); + + Assert.False(frozen.TryGetValue("ab", out string actualValue)); + Assert.Null(actualValue); + + Assert.True(frozen.TryGetValue("ABC", out actualValue)); + Assert.Equal("abc", actualValue); + } + } + + public class FrozenSet_Generic_Tests_string_NonDefault : FrozenSet_Generic_Tests_string + { + protected override IEqualityComparer GetIEqualityComparer() => NonDefaultEqualityComparer.Instance; + } + + public class FrozenSet_Generic_Tests_ulong : FrozenSet_Generic_Tests + { + protected override bool DefaultValueAllowed => true; + + protected override ulong CreateT(int seed) + { + Random rand = new Random(seed); + ulong hi = unchecked((ulong)rand.Next()); + ulong lo = unchecked((ulong)rand.Next()); + return (hi << 32) | lo; + } + } + + public class FrozenSet_Generic_Tests_int : FrozenSet_Generic_Tests + { + protected override bool DefaultValueAllowed => true; + + protected override int CreateT(int seed) => new Random(seed).Next(); + } + + public class FrozenSet_Generic_Tests_SimpleClass : FrozenSet_Generic_Tests + { + protected override SimpleClass CreateT(int seed) + { + int stringLength = seed % 10 + 5; + Random rand = new Random(seed); + byte[] bytes1 = new byte[stringLength]; + rand.NextBytes(bytes1); + return new SimpleClass { Value = Convert.ToBase64String(bytes1) }; + } + } + + public class FrozenSet_NonGeneric_Tests : ICollection_NonGeneric_Tests + { + protected override ICollection NonGenericICollectionFactory() => + Array.Empty().ToFrozenSet(); + + protected override ICollection NonGenericICollectionFactory(int count) + { + var set = new HashSet(); + var rand = new Random(42); + while (set.Count < count) + { + set.Add(rand.Next().ToString(CultureInfo.InvariantCulture)); + } + return set.ToFrozenSet(); + } + + protected override bool IsReadOnly => true; + + protected override bool Enumerator_Current_UndefinedOperation_Throws => true; + + protected override bool ResetImplemented => true; + + protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) => Array.Empty(); + + protected override void AddToCollection(ICollection collection, int numberOfItemsToAdd) => throw new NotImplementedException(); + + protected override Type ICollection_NonGeneric_CopyTo_ArrayOfIncorrectReferenceType_ThrowType => typeof(InvalidCastException); + + protected override Type ICollection_NonGeneric_CopyTo_ArrayOfIncorrectValueType_ThrowType => typeof(InvalidCastException); + + protected override Type ICollection_NonGeneric_CopyTo_NonZeroLowerBound_ThrowType => typeof(ArgumentOutOfRangeException); + } +} diff --git a/src/libraries/System.Collections.Immutable/tests/System.Collections.Immutable.Tests.csproj b/src/libraries/System.Collections.Immutable/tests/System.Collections.Immutable.Tests.csproj index fc886aaeaac34..f5e57da3629f5 100644 --- a/src/libraries/System.Collections.Immutable/tests/System.Collections.Immutable.Tests.csproj +++ b/src/libraries/System.Collections.Immutable/tests/System.Collections.Immutable.Tests.csproj @@ -8,9 +8,7 @@ - + @@ -38,43 +36,28 @@ - - + + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/src/libraries/System.Private.CoreLib/src/System/Collections/HashHelpers.cs b/src/libraries/System.Private.CoreLib/src/System/Collections/HashHelpers.cs index a70b9bf5c0c89..510ba278f30d2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Collections/HashHelpers.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Collections/HashHelpers.cs @@ -28,7 +28,7 @@ internal static partial class HashHelpers // h1(key) + i*h2(key), 0 <= i < size. h2 and the size must be relatively prime. // We prefer the low computation costs of higher prime numbers over the increased // memory allocation of a fixed prime number i.e. when right sizing a HashSet. - private static readonly int[] s_primes = + internal static readonly int[] s_primes = { 3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919, 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591,