diff --git a/src/libraries/System.Collections/ref/System.Collections.cs b/src/libraries/System.Collections/ref/System.Collections.cs index 3625b253780b12..bdf0bc4949acd4 100644 --- a/src/libraries/System.Collections/ref/System.Collections.cs +++ b/src/libraries/System.Collections/ref/System.Collections.cs @@ -618,16 +618,12 @@ namespace System.Collections.Generic public static partial class CollectionExtensions { public static void AddRange(this System.Collections.Generic.List list, params System.ReadOnlySpan source) { } - public static System.Collections.Generic.Dictionary.AlternateLookup GetAlternateLookup(this System.Collections.Generic.Dictionary dictionary) where TKey : notnull where TAlternateKey : notnull, allows ref struct { throw null; } - public static System.Collections.Generic.HashSet.AlternateLookup GetAlternateLookup(this System.Collections.Generic.HashSet set) where TAlternate : allows ref struct { throw null; } public static void CopyTo(this System.Collections.Generic.List list, System.Span destination) { } public static TValue? GetValueOrDefault(this System.Collections.Generic.IReadOnlyDictionary dictionary, TKey key) { throw null; } public static TValue GetValueOrDefault(this System.Collections.Generic.IReadOnlyDictionary dictionary, TKey key, TValue defaultValue) { throw null; } public static void InsertRange(this System.Collections.Generic.List list, int index, params System.ReadOnlySpan source) { } public static bool Remove(this System.Collections.Generic.IDictionary dictionary, TKey key, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TValue value) { throw null; } public static bool TryAdd(this System.Collections.Generic.IDictionary dictionary, TKey key, TValue value) { throw null; } - public static bool TryGetAlternateLookup(this System.Collections.Generic.Dictionary dictionary, out System.Collections.Generic.Dictionary.AlternateLookup lookup) where TKey : notnull where TAlternateKey : notnull, allows ref struct { throw null; } - public static bool TryGetAlternateLookup(this System.Collections.Generic.HashSet set, out System.Collections.Generic.HashSet.AlternateLookup lookup) where TAlternate : allows ref struct { throw null; } public static System.Collections.ObjectModel.ReadOnlyCollection AsReadOnly(this IList list) { throw null; } public static System.Collections.ObjectModel.ReadOnlyDictionary AsReadOnly(this IDictionary dictionary) where TKey : notnull { throw null; } } @@ -675,6 +671,7 @@ public void Clear() { } public bool ContainsKey(TKey key) { throw null; } public bool ContainsValue(TValue value) { throw null; } public int EnsureCapacity(int capacity) { throw null; } + public System.Collections.Generic.Dictionary.AlternateLookup GetAlternateLookup() where TAlternateKey : notnull, allows ref struct { throw null; } public System.Collections.Generic.Dictionary.Enumerator GetEnumerator() { throw null; } [System.ObsoleteAttribute("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.", DiagnosticId = "SYSLIB0051", UrlFormat = "https://aka.ms/dotnet-warnings/{0}")] [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] @@ -696,6 +693,7 @@ void System.Collections.IDictionary.Remove(object key) { } public void TrimExcess() { } public void TrimExcess(int capacity) { } public bool TryAdd(TKey key, TValue value) { throw null; } + public bool TryGetAlternateLookup(out System.Collections.Generic.Dictionary.AlternateLookup lookup) where TAlternateKey : notnull, allows ref struct { throw null; } public bool TryGetValue(TKey key, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TValue value) { throw null; } public readonly partial struct AlternateLookup where TAlternateKey : notnull, allows ref struct { @@ -812,6 +810,7 @@ public void CopyTo(T[] array, int arrayIndex, int count) { } public static System.Collections.Generic.IEqualityComparer> CreateSetComparer() { throw null; } public int EnsureCapacity(int capacity) { throw null; } public void ExceptWith(System.Collections.Generic.IEnumerable other) { } + public System.Collections.Generic.HashSet.AlternateLookup GetAlternateLookup() where TAlternate : allows ref struct { throw null; } public System.Collections.Generic.HashSet.Enumerator GetEnumerator() { throw null; } [System.ObsoleteAttribute("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.", DiagnosticId = "SYSLIB0051", UrlFormat = "https://aka.ms/dotnet-warnings/{0}")] [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] @@ -832,6 +831,7 @@ void System.Collections.Generic.ICollection.Add(T item) { } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } public void TrimExcess() { } public void TrimExcess(int capacity) { } + public bool TryGetAlternateLookup(out System.Collections.Generic.HashSet.AlternateLookup lookup) where TAlternate : allows ref struct { throw null; } public bool TryGetValue(T equalValue, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out T actualValue) { throw null; } public void UnionWith(System.Collections.Generic.IEnumerable other) { } public readonly partial struct AlternateLookup where TAlternate : allows ref struct diff --git a/src/libraries/System.Collections/tests/Generic/CollectionExtensionsTests.cs b/src/libraries/System.Collections/tests/Generic/CollectionExtensionsTests.cs index b43f5affdaee59..d07e031f0e7acf 100644 --- a/src/libraries/System.Collections/tests/Generic/CollectionExtensionsTests.cs +++ b/src/libraries/System.Collections/tests/Generic/CollectionExtensionsTests.cs @@ -141,252 +141,6 @@ public void AsReadOnly_NullIDictionary_ThrowsArgumentNullException() Assert.Throws("dictionary", () => dictionary.AsReadOnly()); } - [Fact] - public void GetAlternateLookup_ThrowsWhenNull() - { - AssertExtensions.Throws("dictionary", () => CollectionExtensions.GetAlternateLookup((Dictionary)null)); - AssertExtensions.Throws("dictionary", () => CollectionExtensions.TryGetAlternateLookup((Dictionary)null, out _)); - - AssertExtensions.Throws("set", () => CollectionExtensions.GetAlternateLookup((HashSet)null)); - AssertExtensions.Throws("set", () => CollectionExtensions.TryGetAlternateLookup((HashSet)null, out _)); - } - - [Fact] - public void GetAlternateLookup_FailsWhenIncompatible() - { - var dictionary = new Dictionary(StringComparer.Ordinal); - var hashSet = new HashSet(StringComparer.Ordinal); - - dictionary.GetAlternateLookup>(); - Assert.True(dictionary.TryGetAlternateLookup>(out _)); - - hashSet.GetAlternateLookup>(); - Assert.True(hashSet.TryGetAlternateLookup>(out _)); - - Assert.Throws(() => dictionary.GetAlternateLookup>()); - Assert.Throws(() => dictionary.GetAlternateLookup()); - Assert.Throws(() => dictionary.GetAlternateLookup()); - - Assert.False(dictionary.TryGetAlternateLookup>(out _)); - Assert.False(dictionary.TryGetAlternateLookup(out _)); - Assert.False(dictionary.TryGetAlternateLookup(out _)); - - Assert.Throws(() => hashSet.GetAlternateLookup>()); - Assert.Throws(() => hashSet.GetAlternateLookup()); - Assert.Throws(() => hashSet.GetAlternateLookup()); - - Assert.False(hashSet.TryGetAlternateLookup>(out _)); - Assert.False(hashSet.TryGetAlternateLookup(out _)); - Assert.False(hashSet.TryGetAlternateLookup(out _)); - } - - public static IEnumerable Dictionary_GetAlternateLookup_OperationsMatchUnderlyingDictionary_MemberData() - { - yield return new object[] { EqualityComparer.Default }; - yield return new object[] { StringComparer.Ordinal }; - yield return new object[] { StringComparer.OrdinalIgnoreCase }; - yield return new object[] { StringComparer.InvariantCulture }; - yield return new object[] { StringComparer.InvariantCultureIgnoreCase }; - yield return new object[] { StringComparer.CurrentCulture }; - yield return new object[] { StringComparer.CurrentCultureIgnoreCase }; - } - - [Theory] - [MemberData(nameof(Dictionary_GetAlternateLookup_OperationsMatchUnderlyingDictionary_MemberData))] - public void Dictionary_GetAlternateLookup_OperationsMatchUnderlyingDictionary(IEqualityComparer comparer) - { - // Test with a variety of comparers to ensure that the alternate lookup is consistent with the underlying dictionary - Dictionary dictionary = new(comparer); - Dictionary.AlternateLookup> lookup = dictionary.GetAlternateLookup>(); - Assert.Same(dictionary, lookup.Dictionary); - Assert.Same(lookup.Dictionary, lookup.Dictionary); - - string actualKey; - int value; - - // Add to the dictionary and validate that the lookup reflects the changes - dictionary["123"] = 123; - Assert.True(lookup.ContainsKey("123".AsSpan())); - Assert.True(lookup.TryGetValue("123".AsSpan(), out value)); - Assert.Equal(123, value); - Assert.Equal(123, lookup["123".AsSpan()]); - Assert.False(lookup.TryAdd("123".AsSpan(), 321)); - Assert.True(lookup.Remove("123".AsSpan())); - Assert.False(dictionary.ContainsKey("123")); - Assert.Throws(() => lookup["123".AsSpan()]); - - // Add via the lookup and validate that the dictionary reflects the changes - Assert.True(lookup.TryAdd("123".AsSpan(), 123)); - Assert.True(dictionary.ContainsKey("123")); - lookup.TryGetValue("123".AsSpan(), out value); - Assert.Equal(123, value); - Assert.False(lookup.Remove("321".AsSpan(), out actualKey, out value)); - Assert.Null(actualKey); - Assert.Equal(0, value); - Assert.True(lookup.Remove("123".AsSpan(), out actualKey, out value)); - Assert.Equal("123", actualKey); - Assert.Equal(123, value); - - // Ensure that case-sensitivity of the comparer is respected - lookup["a".AsSpan()] = 42; - if (dictionary.Comparer.Equals(EqualityComparer.Default) || - dictionary.Comparer.Equals(StringComparer.Ordinal) || - dictionary.Comparer.Equals(StringComparer.InvariantCulture) || - dictionary.Comparer.Equals(StringComparer.CurrentCulture)) - { - Assert.True(lookup.TryGetValue("a".AsSpan(), out actualKey, out value)); - Assert.Equal("a", actualKey); - Assert.Equal(42, value); - Assert.True(lookup.TryAdd("A".AsSpan(), 42)); - Assert.True(lookup.Remove("a".AsSpan())); - Assert.False(lookup.Remove("a".AsSpan())); - Assert.True(lookup.Remove("A".AsSpan())); - } - else - { - Assert.True(lookup.TryGetValue("A".AsSpan(), out actualKey, out value)); - Assert.Equal("a", actualKey); - Assert.Equal(42, value); - Assert.False(lookup.TryAdd("A".AsSpan(), 42)); - Assert.True(lookup.Remove("A".AsSpan())); - Assert.False(lookup.Remove("a".AsSpan())); - Assert.False(lookup.Remove("A".AsSpan())); - } - - // Validate overwrites - lookup["a".AsSpan()] = 42; - Assert.Equal(42, dictionary["a"]); - lookup["a".AsSpan()] = 43; - Assert.True(lookup.Remove("a".AsSpan(), out actualKey, out value)); - Assert.Equal("a", actualKey); - Assert.Equal(43, value); - - // Test adding multiple entries via the lookup - for (int i = 0; i < 10; i++) - { - Assert.Equal(i, dictionary.Count); - Assert.True(lookup.TryAdd(i.ToString().AsSpan(), i)); - Assert.False(lookup.TryAdd(i.ToString().AsSpan(), i)); - } - - Assert.Equal(10, dictionary.Count); - - // Test that the lookup and the dictionary agree on what's in and not in - for (int i = -1; i <= 10; i++) - { - Assert.Equal(dictionary.TryGetValue(i.ToString(), out int dv), lookup.TryGetValue(i.ToString().AsSpan(), out int lv)); - Assert.Equal(dv, lv); - } - - // Test removing multiple entries via the lookup - for (int i = 9; i >= 0; i--) - { - Assert.True(lookup.Remove(i.ToString().AsSpan(), out actualKey, out value)); - Assert.Equal(i.ToString(), actualKey); - Assert.Equal(i, value); - Assert.False(lookup.Remove(i.ToString().AsSpan(), out actualKey, out value)); - Assert.Null(actualKey); - Assert.Equal(0, value); - Assert.Equal(i, dictionary.Count); - } - } - - [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(2)] - [InlineData(3)] - [InlineData(4)] - [InlineData(5)] - public void HashSet_GetAlternateLookup_OperationsMatchUnderlyingSet(int mode) - { - // Test with a variety of comparers to ensure that the alternate lookup is consistent with the underlying set - HashSet set = new(mode switch - { - 0 => StringComparer.Ordinal, - 1 => StringComparer.OrdinalIgnoreCase, - 2 => StringComparer.InvariantCulture, - 3 => StringComparer.InvariantCultureIgnoreCase, - 4 => StringComparer.CurrentCulture, - 5 => StringComparer.CurrentCultureIgnoreCase, - _ => throw new ArgumentOutOfRangeException(nameof(mode)) - }); - HashSet.AlternateLookup> lookup = set.GetAlternateLookup>(); - Assert.Same(set, lookup.Set); - Assert.Same(lookup.Set, lookup.Set); - - // Add to the set and validate that the lookup reflects the changes - Assert.True(set.Add("123")); - Assert.True(lookup.Contains("123".AsSpan())); - Assert.False(lookup.Add("123".AsSpan())); - Assert.True(lookup.Remove("123".AsSpan())); - Assert.False(set.Contains("123")); - - // Add via the lookup and validate that the set reflects the changes - Assert.True(lookup.Add("123".AsSpan())); - Assert.True(set.Contains("123")); - lookup.TryGetValue("123".AsSpan(), out string value); - Assert.Equal("123", value); - Assert.False(lookup.Remove("321".AsSpan())); - Assert.True(lookup.Remove("123".AsSpan())); - - // Ensure that case-sensitivity of the comparer is respected - Assert.True(lookup.Add("a")); - if (set.Comparer.Equals(StringComparer.Ordinal) || - set.Comparer.Equals(StringComparer.InvariantCulture) || - set.Comparer.Equals(StringComparer.CurrentCulture)) - { - Assert.True(lookup.Add("A".AsSpan())); - Assert.True(lookup.Remove("a".AsSpan())); - Assert.False(lookup.Remove("a".AsSpan())); - Assert.True(lookup.Remove("A".AsSpan())); - } - else - { - Assert.False(lookup.Add("A".AsSpan())); - Assert.True(lookup.Remove("A".AsSpan())); - Assert.False(lookup.Remove("a".AsSpan())); - Assert.False(lookup.Remove("A".AsSpan())); - } - - // Test the behavior of null vs "" in the set and lookup - Assert.True(set.Add(null)); - Assert.True(set.Add(string.Empty)); - Assert.True(set.Contains(null)); - Assert.True(set.Contains("")); - Assert.True(lookup.Contains("".AsSpan())); - Assert.True(lookup.Remove("".AsSpan())); - Assert.Equal(1, set.Count); - Assert.False(lookup.Remove("".AsSpan())); - Assert.True(set.Remove(null)); - Assert.Equal(0, set.Count); - - // Test adding multiple entries via the lookup - for (int i = 0; i < 10; i++) - { - Assert.Equal(i, set.Count); - Assert.True(lookup.Add(i.ToString().AsSpan())); - Assert.False(lookup.Add(i.ToString().AsSpan())); - } - - Assert.Equal(10, set.Count); - - // Test that the lookup and the set agree on what's in and not in - for (int i = -1; i <= 10; i++) - { - Assert.Equal(set.TryGetValue(i.ToString(), out string dv), lookup.TryGetValue(i.ToString().AsSpan(), out string lv)); - Assert.Equal(dv, lv); - } - - // Test removing multiple entries via the lookup - for (int i = 9; i >= 0; i--) - { - Assert.True(lookup.Remove(i.ToString().AsSpan())); - Assert.False(lookup.Remove(i.ToString().AsSpan())); - Assert.Equal(i, set.Count); - } - } - [Fact] public void Dictionary_NotCorruptedByThrowingComparer() { @@ -394,7 +148,7 @@ public void Dictionary_NotCorruptedByThrowingComparer() Assert.Equal(0, dict.Count); - Assert.Throws(() => dict.GetAlternateLookup>().TryAdd("123".AsSpan(), "123")); + Assert.Throws(() => dict.GetAlternateLookup>().TryAdd("123".AsSpan(), "123")); Assert.Equal(0, dict.Count); dict.Add("123", "123"); @@ -408,7 +162,7 @@ public void Dictionary_NotCorruptedByNullReturningComparer() Assert.Equal(0, dict.Count); - Assert.ThrowsAny(() => dict.GetAlternateLookup>().TryAdd("123".AsSpan(), "123")); + Assert.ThrowsAny(() => dict.GetAlternateLookup>().TryAdd("123".AsSpan(), "123")); Assert.Equal(0, dict.Count); dict.Add("123", "123"); @@ -422,7 +176,7 @@ public void HashSet_NotCorruptedByThrowingComparer() Assert.Equal(0, set.Count); - Assert.Throws(() => set.GetAlternateLookup>().Add("123".AsSpan())); + Assert.Throws(() => set.GetAlternateLookup>().Add("123".AsSpan())); Assert.Equal(0, set.Count); set.Add("123"); diff --git a/src/libraries/System.Collections/tests/Generic/Dictionary/Dictionary.Generic.Tests.cs b/src/libraries/System.Collections/tests/Generic/Dictionary/Dictionary.Generic.Tests.cs index aa6133538895d4..470312be885c26 100644 --- a/src/libraries/System.Collections/tests/Generic/Dictionary/Dictionary.Generic.Tests.cs +++ b/src/libraries/System.Collections/tests/Generic/Dictionary/Dictionary.Generic.Tests.cs @@ -643,6 +643,136 @@ public void TrimExcess_Generic_DoesInvalidateEnumeration() #endregion + #region GetAlternateComparer + [Fact] + public void GetAlternateLookup_FailsWhenIncompatible() + { + var dictionary = new Dictionary(StringComparer.Ordinal); + + dictionary.GetAlternateLookup>(); + Assert.True(dictionary.TryGetAlternateLookup>(out _)); + + Assert.Throws(() => dictionary.GetAlternateLookup>()); + Assert.Throws(() => dictionary.GetAlternateLookup()); + Assert.Throws(() => dictionary.GetAlternateLookup()); + + Assert.False(dictionary.TryGetAlternateLookup>(out _)); + Assert.False(dictionary.TryGetAlternateLookup(out _)); + Assert.False(dictionary.TryGetAlternateLookup(out _)); + } + + public static IEnumerable Dictionary_GetAlternateLookup_OperationsMatchUnderlyingDictionary_MemberData() + { + yield return new object[] { EqualityComparer.Default }; + yield return new object[] { StringComparer.Ordinal }; + yield return new object[] { StringComparer.OrdinalIgnoreCase }; + yield return new object[] { StringComparer.InvariantCulture }; + yield return new object[] { StringComparer.InvariantCultureIgnoreCase }; + yield return new object[] { StringComparer.CurrentCulture }; + yield return new object[] { StringComparer.CurrentCultureIgnoreCase }; + } + + [Theory] + [MemberData(nameof(Dictionary_GetAlternateLookup_OperationsMatchUnderlyingDictionary_MemberData))] + public void Dictionary_GetAlternateLookup_OperationsMatchUnderlyingDictionary(IEqualityComparer comparer) + { + // Test with a variety of comparers to ensure that the alternate lookup is consistent with the underlying dictionary + Dictionary dictionary = new(comparer); + Dictionary.AlternateLookup> lookup = dictionary.GetAlternateLookup>(); + Assert.Same(dictionary, lookup.Dictionary); + Assert.Same(lookup.Dictionary, lookup.Dictionary); + + string actualKey; + int value; + + // Add to the dictionary and validate that the lookup reflects the changes + dictionary["123"] = 123; + Assert.True(lookup.ContainsKey("123".AsSpan())); + Assert.True(lookup.TryGetValue("123".AsSpan(), out value)); + Assert.Equal(123, value); + Assert.Equal(123, lookup["123".AsSpan()]); + Assert.False(lookup.TryAdd("123".AsSpan(), 321)); + Assert.True(lookup.Remove("123".AsSpan())); + Assert.False(dictionary.ContainsKey("123")); + Assert.Throws(() => lookup["123".AsSpan()]); + + // Add via the lookup and validate that the dictionary reflects the changes + Assert.True(lookup.TryAdd("123".AsSpan(), 123)); + Assert.True(dictionary.ContainsKey("123")); + lookup.TryGetValue("123".AsSpan(), out value); + Assert.Equal(123, value); + Assert.False(lookup.Remove("321".AsSpan(), out actualKey, out value)); + Assert.Null(actualKey); + Assert.Equal(0, value); + Assert.True(lookup.Remove("123".AsSpan(), out actualKey, out value)); + Assert.Equal("123", actualKey); + Assert.Equal(123, value); + + // Ensure that case-sensitivity of the comparer is respected + lookup["a".AsSpan()] = 42; + if (dictionary.Comparer.Equals(EqualityComparer.Default) || + dictionary.Comparer.Equals(StringComparer.Ordinal) || + dictionary.Comparer.Equals(StringComparer.InvariantCulture) || + dictionary.Comparer.Equals(StringComparer.CurrentCulture)) + { + Assert.True(lookup.TryGetValue("a".AsSpan(), out actualKey, out value)); + Assert.Equal("a", actualKey); + Assert.Equal(42, value); + Assert.True(lookup.TryAdd("A".AsSpan(), 42)); + Assert.True(lookup.Remove("a".AsSpan())); + Assert.False(lookup.Remove("a".AsSpan())); + Assert.True(lookup.Remove("A".AsSpan())); + } + else + { + Assert.True(lookup.TryGetValue("A".AsSpan(), out actualKey, out value)); + Assert.Equal("a", actualKey); + Assert.Equal(42, value); + Assert.False(lookup.TryAdd("A".AsSpan(), 42)); + Assert.True(lookup.Remove("A".AsSpan())); + Assert.False(lookup.Remove("a".AsSpan())); + Assert.False(lookup.Remove("A".AsSpan())); + } + + // Validate overwrites + lookup["a".AsSpan()] = 42; + Assert.Equal(42, dictionary["a"]); + lookup["a".AsSpan()] = 43; + Assert.True(lookup.Remove("a".AsSpan(), out actualKey, out value)); + Assert.Equal("a", actualKey); + Assert.Equal(43, value); + + // Test adding multiple entries via the lookup + for (int i = 0; i < 10; i++) + { + Assert.Equal(i, dictionary.Count); + Assert.True(lookup.TryAdd(i.ToString().AsSpan(), i)); + Assert.False(lookup.TryAdd(i.ToString().AsSpan(), i)); + } + + Assert.Equal(10, dictionary.Count); + + // Test that the lookup and the dictionary agree on what's in and not in + for (int i = -1; i <= 10; i++) + { + Assert.Equal(dictionary.TryGetValue(i.ToString(), out int dv), lookup.TryGetValue(i.ToString().AsSpan(), out int lv)); + Assert.Equal(dv, lv); + } + + // Test removing multiple entries via the lookup + for (int i = 9; i >= 0; i--) + { + Assert.True(lookup.Remove(i.ToString().AsSpan(), out actualKey, out value)); + Assert.Equal(i.ToString(), actualKey); + Assert.Equal(i, value); + Assert.False(lookup.Remove(i.ToString().AsSpan(), out actualKey, out value)); + Assert.Null(actualKey); + Assert.Equal(0, value); + Assert.Equal(i, dictionary.Count); + } + } + #endregion + #region Non-randomized comparers [Fact] public void Dictionary_Comparer_NonRandomizedStringComparers() diff --git a/src/libraries/System.Collections/tests/Generic/Dictionary/HashCollisionScenarios/OutOfBoundsRegression.cs b/src/libraries/System.Collections/tests/Generic/Dictionary/HashCollisionScenarios/OutOfBoundsRegression.cs index 12e979cf40da27..e131d4bb25ee5d 100644 --- a/src/libraries/System.Collections/tests/Generic/Dictionary/HashCollisionScenarios/OutOfBoundsRegression.cs +++ b/src/libraries/System.Collections/tests/Generic/Dictionary/HashCollisionScenarios/OutOfBoundsRegression.cs @@ -17,7 +17,7 @@ public class InternalHashCodeTests_Dictionary_NullComparer : InternalHashCodeTes protected override void AddKey(Dictionary collection, string key) => collection.Add(key, key); protected override bool ContainsKey(Dictionary collection, string key) => collection.ContainsKey(key); protected override bool ContainsKey(Dictionary collection, ReadOnlySpan key) => - collection.GetAlternateLookup>().ContainsKey(key); + collection.GetAlternateLookup>().ContainsKey(key); protected override IEqualityComparer GetComparer(Dictionary collection) => collection.Comparer; protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedOrdinalComparerType; @@ -61,7 +61,7 @@ public class InternalHashCodeTests_Dictionary_DefaultComparer : InternalHashCode protected override void AddKey(Dictionary collection, string key) => collection.Add(key, key); protected override bool ContainsKey(Dictionary collection, string key) => collection.ContainsKey(key); protected override bool ContainsKey(Dictionary collection, ReadOnlySpan key) => - collection.GetAlternateLookup>().ContainsKey(key); + collection.GetAlternateLookup>().ContainsKey(key); protected override IEqualityComparer GetComparer(Dictionary collection) => collection.Comparer; protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedOrdinalComparerType; @@ -75,7 +75,7 @@ public class InternalHashCodeTests_Dictionary_OrdinalComparer : InternalHashCode protected override void AddKey(Dictionary collection, string key) => collection.Add(key, key); protected override bool ContainsKey(Dictionary collection, string key) => collection.ContainsKey(key); protected override bool ContainsKey(Dictionary collection, ReadOnlySpan key) => - collection.GetAlternateLookup>().ContainsKey(key); + collection.GetAlternateLookup>().ContainsKey(key); protected override IEqualityComparer GetComparer(Dictionary collection) => collection.Comparer; protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedOrdinalComparerType; @@ -89,7 +89,7 @@ public class InternalHashCodeTests_Dictionary_OrdinalIgnoreCaseComparer : Intern protected override void AddKey(Dictionary collection, string key) => collection.Add(key, key); protected override bool ContainsKey(Dictionary collection, string key) => collection.ContainsKey(key); protected override bool ContainsKey(Dictionary collection, ReadOnlySpan key) => - collection.GetAlternateLookup>().ContainsKey(key); + collection.GetAlternateLookup>().ContainsKey(key); protected override IEqualityComparer GetComparer(Dictionary collection) => collection.Comparer; protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedOrdinalIgnoreCaseComparerType; @@ -103,7 +103,7 @@ public class InternalHashCodeTests_Dictionary_LinguisticComparer : InternalHashC protected override void AddKey(Dictionary collection, string key) => collection.Add(key, key); protected override bool ContainsKey(Dictionary collection, string key) => collection.ContainsKey(key); protected override bool ContainsKey(Dictionary collection, ReadOnlySpan key) => - collection.GetAlternateLookup>().ContainsKey(key); + collection.GetAlternateLookup>().ContainsKey(key); protected override IEqualityComparer GetComparer(Dictionary collection) => collection.Comparer; protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => StringComparer.InvariantCulture.GetType(); @@ -119,7 +119,7 @@ public class InternalHashCodeTests_Dictionary_GetValueRefOrAddDefault : Internal protected override void AddKey(Dictionary collection, string key) => CollectionsMarshal.GetValueRefOrAddDefault(collection, key, out _) = null; protected override bool ContainsKey(Dictionary collection, string key) => collection.ContainsKey(key); protected override bool ContainsKey(Dictionary collection, ReadOnlySpan key) => - collection.GetAlternateLookup>().ContainsKey(key); + collection.GetAlternateLookup>().ContainsKey(key); protected override IEqualityComparer GetComparer(Dictionary collection) => collection.Comparer; protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedOrdinalComparerType; @@ -135,7 +135,7 @@ public class InternalHashCodeTests_HashSet_NullComparer : InternalHashCodeTests< protected override void AddKey(HashSet collection, string key) => collection.Add(key); protected override bool ContainsKey(HashSet collection, string key) => collection.Contains(key); protected override bool ContainsKey(HashSet collection, ReadOnlySpan key) => - collection.GetAlternateLookup>().Contains(key); + collection.GetAlternateLookup>().Contains(key); protected override IEqualityComparer GetComparer(HashSet collection) => collection.Comparer; protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedOrdinalComparerType; @@ -149,7 +149,7 @@ public class InternalHashCodeTests_HashSet_DefaultComparer : InternalHashCodeTes protected override void AddKey(HashSet collection, string key) => collection.Add(key); protected override bool ContainsKey(HashSet collection, string key) => collection.Contains(key); protected override bool ContainsKey(HashSet collection, ReadOnlySpan key) => - collection.GetAlternateLookup>().Contains(key); + collection.GetAlternateLookup>().Contains(key); protected override IEqualityComparer GetComparer(HashSet collection) => collection.Comparer; protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedOrdinalComparerType; @@ -163,7 +163,7 @@ public class InternalHashCodeTests_HashSet_OrdinalComparer : InternalHashCodeTes protected override void AddKey(HashSet collection, string key) => collection.Add(key); protected override bool ContainsKey(HashSet collection, string key) => collection.Contains(key); protected override bool ContainsKey(HashSet collection, ReadOnlySpan key) => - collection.GetAlternateLookup>().Contains(key); + collection.GetAlternateLookup>().Contains(key); protected override IEqualityComparer GetComparer(HashSet collection) => collection.Comparer; protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedOrdinalComparerType; @@ -177,7 +177,7 @@ public class InternalHashCodeTests_HashSet_OrdinalIgnoreCaseComparer : InternalH protected override void AddKey(HashSet collection, string key) => collection.Add(key); protected override bool ContainsKey(HashSet collection, string key) => collection.Contains(key); protected override bool ContainsKey(HashSet collection, ReadOnlySpan key) => - collection.GetAlternateLookup>().Contains(key); + collection.GetAlternateLookup>().Contains(key); protected override IEqualityComparer GetComparer(HashSet collection) => collection.Comparer; protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedOrdinalIgnoreCaseComparerType; @@ -191,7 +191,7 @@ public class InternalHashCodeTests_HashSet_LinguisticComparer : InternalHashCode protected override void AddKey(HashSet collection, string key) => collection.Add(key); protected override bool ContainsKey(HashSet collection, string key) => collection.Contains(key); protected override bool ContainsKey(HashSet collection, ReadOnlySpan key) => - collection.GetAlternateLookup>().Contains(key); + collection.GetAlternateLookup>().Contains(key); protected override IEqualityComparer GetComparer(HashSet collection) => collection.Comparer; protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => StringComparer.InvariantCulture.GetType(); diff --git a/src/libraries/System.Collections/tests/Generic/HashSet/HashSet.Generic.Tests.cs b/src/libraries/System.Collections/tests/Generic/HashSet/HashSet.Generic.Tests.cs index 30b387a486fbd1..e3d4829cdaba17 100644 --- a/src/libraries/System.Collections/tests/Generic/HashSet/HashSet.Generic.Tests.cs +++ b/src/libraries/System.Collections/tests/Generic/HashSet/HashSet.Generic.Tests.cs @@ -416,6 +416,121 @@ public void SetComparer_SequenceEqualTests() #endregion + #region GetAlternateLookup + [Fact] + public void GetAlternateLookup_FailsWhenIncompatible() + { + var hashSet = new HashSet(StringComparer.Ordinal); + + hashSet.GetAlternateLookup>(); + Assert.True(hashSet.TryGetAlternateLookup>(out _)); + + Assert.Throws(() => hashSet.GetAlternateLookup>()); + Assert.Throws(() => hashSet.GetAlternateLookup()); + Assert.Throws(() => hashSet.GetAlternateLookup()); + + Assert.False(hashSet.TryGetAlternateLookup>(out _)); + Assert.False(hashSet.TryGetAlternateLookup(out _)); + Assert.False(hashSet.TryGetAlternateLookup(out _)); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + public void HashSet_GetAlternateLookup_OperationsMatchUnderlyingSet(int mode) + { + // Test with a variety of comparers to ensure that the alternate lookup is consistent with the underlying set + HashSet set = new(mode switch + { + 0 => StringComparer.Ordinal, + 1 => StringComparer.OrdinalIgnoreCase, + 2 => StringComparer.InvariantCulture, + 3 => StringComparer.InvariantCultureIgnoreCase, + 4 => StringComparer.CurrentCulture, + 5 => StringComparer.CurrentCultureIgnoreCase, + _ => throw new ArgumentOutOfRangeException(nameof(mode)) + }); + HashSet.AlternateLookup> lookup = set.GetAlternateLookup>(); + Assert.Same(set, lookup.Set); + Assert.Same(lookup.Set, lookup.Set); + + // Add to the set and validate that the lookup reflects the changes + Assert.True(set.Add("123")); + Assert.True(lookup.Contains("123".AsSpan())); + Assert.False(lookup.Add("123".AsSpan())); + Assert.True(lookup.Remove("123".AsSpan())); + Assert.False(set.Contains("123")); + + // Add via the lookup and validate that the set reflects the changes + Assert.True(lookup.Add("123".AsSpan())); + Assert.True(set.Contains("123")); + lookup.TryGetValue("123".AsSpan(), out string value); + Assert.Equal("123", value); + Assert.False(lookup.Remove("321".AsSpan())); + Assert.True(lookup.Remove("123".AsSpan())); + + // Ensure that case-sensitivity of the comparer is respected + Assert.True(lookup.Add("a")); + if (set.Comparer.Equals(StringComparer.Ordinal) || + set.Comparer.Equals(StringComparer.InvariantCulture) || + set.Comparer.Equals(StringComparer.CurrentCulture)) + { + Assert.True(lookup.Add("A".AsSpan())); + Assert.True(lookup.Remove("a".AsSpan())); + Assert.False(lookup.Remove("a".AsSpan())); + Assert.True(lookup.Remove("A".AsSpan())); + } + else + { + Assert.False(lookup.Add("A".AsSpan())); + Assert.True(lookup.Remove("A".AsSpan())); + Assert.False(lookup.Remove("a".AsSpan())); + Assert.False(lookup.Remove("A".AsSpan())); + } + + // Test the behavior of null vs "" in the set and lookup + Assert.True(set.Add(null)); + Assert.True(set.Add(string.Empty)); + Assert.True(set.Contains(null)); + Assert.True(set.Contains("")); + Assert.True(lookup.Contains("".AsSpan())); + Assert.True(lookup.Remove("".AsSpan())); + Assert.Equal(1, set.Count); + Assert.False(lookup.Remove("".AsSpan())); + Assert.True(set.Remove(null)); + Assert.Equal(0, set.Count); + + // Test adding multiple entries via the lookup + for (int i = 0; i < 10; i++) + { + Assert.Equal(i, set.Count); + Assert.True(lookup.Add(i.ToString().AsSpan())); + Assert.False(lookup.Add(i.ToString().AsSpan())); + } + + Assert.Equal(10, set.Count); + + // Test that the lookup and the set agree on what's in and not in + for (int i = -1; i <= 10; i++) + { + Assert.Equal(set.TryGetValue(i.ToString(), out string dv), lookup.TryGetValue(i.ToString().AsSpan(), out string lv)); + Assert.Equal(dv, lv); + } + + // Test removing multiple entries via the lookup + for (int i = 9; i >= 0; i--) + { + Assert.True(lookup.Remove(i.ToString().AsSpan())); + Assert.False(lookup.Remove(i.ToString().AsSpan())); + Assert.Equal(i, set.Count); + } + } + #endregion + [Fact] public void CanBeCastedToISet() { diff --git a/src/libraries/System.Console/src/System/IO/KeyParser.cs b/src/libraries/System.Console/src/System/IO/KeyParser.cs index 2b4cff2502c2c6..317c1a814beb06 100644 --- a/src/libraries/System.Console/src/System/IO/KeyParser.cs +++ b/src/libraries/System.Console/src/System/IO/KeyParser.cs @@ -63,7 +63,7 @@ private static bool TryParseTerminalInputSequence(char[] buffer, TerminalFormatS } Dictionary.AlternateLookup> terminfoDb = // the most important source of truth - terminalFormatStrings.KeyFormatToConsoleKey.GetAlternateLookup>(); + terminalFormatStrings.KeyFormatToConsoleKey.GetAlternateLookup>(); ConsoleModifiers modifiers = ConsoleModifiers.None; ConsoleKey key; diff --git a/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/CollectionExtensions.cs b/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/CollectionExtensions.cs index f2d3e45286042f..cf4256f546d59f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/CollectionExtensions.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/CollectionExtensions.cs @@ -160,127 +160,5 @@ public static void CopyTo(this List list, Span destination) new ReadOnlySpan(list._items, 0, list._size).CopyTo(destination); } - - /// - /// Gets an instance of a type that may be used to perform operations on a - /// using a as a key instead of a . - /// - /// The type of the keys used by the dictionary instance. - /// The type of the values used by the dictionary instance. - /// The alternate type of a key for performing lookups. - /// The dictionary instance. - /// The created lookup instance. - /// is null. - /// 's comparer is not compatible with . - /// - /// The supplied must be using a comparer that implements with - /// and . If it doesn't, an exception will be thrown. - /// - public static Dictionary.AlternateLookup GetAlternateLookup( - this Dictionary dictionary) - where TKey : notnull - where TAlternateKey : notnull, allows ref struct - { - ArgumentNullException.ThrowIfNull(dictionary); - - if (!Dictionary.AlternateLookup.IsCompatibleKey(dictionary)) - { - ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_IncompatibleComparer); - } - - return new Dictionary.AlternateLookup(dictionary); - } - - /// - /// Gets an instance of a type that may be used to perform operations on a - /// using a as a key instead of a . - /// - /// The type of the keys used by the dictionary instance. - /// The type of the values used by the dictionary instance. - /// The alternate type of a key for performing lookups. - /// The dictionary instance. - /// The created lookup instance when the method returns true, or a default instance that should not be used if the method returns false. - /// true if a lookup could be created; otherwise, false. - /// is null. - /// - /// The supplied must be using a comparer that implements with - /// and . If it doesn't, the method will return false. - /// - public static bool TryGetAlternateLookup( - this Dictionary dictionary, - out Dictionary.AlternateLookup lookup) - where TKey : notnull - where TAlternateKey : notnull, allows ref struct - { - ArgumentNullException.ThrowIfNull(dictionary); - - if (Dictionary.AlternateLookup.IsCompatibleKey(dictionary)) - { - lookup = new Dictionary.AlternateLookup(dictionary); - return true; - } - - lookup = default; - return false; - } - - /// - /// Gets an instance of a type that may be used to perform operations on a - /// using a instead of a . - /// - /// The type of the items used by the set instance. - /// The alternate type of instance for performing lookups. - /// The set instance. - /// The created lookup instance. - /// is null. - /// 's comparer is not compatible with . - /// - /// The supplied must be using a comparer that implements with - /// and . If it doesn't, an exception will be thrown. - /// - public static HashSet.AlternateLookup GetAlternateLookup( - this HashSet set) - where TAlternate : allows ref struct - { - ArgumentNullException.ThrowIfNull(set); - - if (!HashSet.AlternateLookup.IsCompatibleItem(set)) - { - ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_IncompatibleComparer); - } - - return new HashSet.AlternateLookup(set); - } - - /// - /// Gets an instance of a type that may be used to perform operations on a - /// using a instead of a . - /// - /// The type of the items used by the set instance. - /// The alternate type of instance for performing lookups. - /// The set instance. - /// The created lookup instance when the method returns true, or a default instance that should not be used if the method returns false. - /// true if a lookup could be created; otherwise, false. - /// is null. - /// - /// The supplied must be using a comparer that implements with - /// and . If it doesn't, the method returns false. - /// - public static bool TryGetAlternateLookup( - this HashSet set, - out HashSet.AlternateLookup lookup) - where TAlternate : allows ref struct - { - ArgumentNullException.ThrowIfNull(set); - - if (HashSet.AlternateLookup.IsCompatibleItem(set)) - { - lookup = new HashSet.AlternateLookup(set); - return true; - } - - lookup = default; - return false; - } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/Dictionary.cs b/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/Dictionary.cs index 87583ded8e4558..c7c17490268da7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/Dictionary.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/Dictionary.cs @@ -635,6 +635,53 @@ private bool TryInsert(TKey key, TValue value, InsertionBehavior behavior) return true; } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + /// The alternate type of a key for performing lookups. + /// The created lookup instance. + /// The dictionary's comparer is not compatible with . + /// + /// The dictionary must be using a comparer that implements with + /// and . If it doesn't, an exception will be thrown. + /// + public AlternateLookup GetAlternateLookup() + where TAlternateKey : notnull, allows ref struct + { + if (!AlternateLookup.IsCompatibleKey(this)) + { + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_IncompatibleComparer); + } + + return new AlternateLookup(this); + } + + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + /// The alternate type of a key for performing lookups. + /// The created lookup instance when the method returns true, or a default instance that should not be used if the method returns false. + /// true if a lookup could be created; otherwise, false. + /// + /// The dictionary must be using a comparer that implements with + /// and . If it doesn't, the method will return false. + /// + public bool TryGetAlternateLookup( + out AlternateLookup lookup) + where TAlternateKey : notnull, allows ref struct + { + if (AlternateLookup.IsCompatibleKey(this)) + { + lookup = new AlternateLookup(this); + return true; + } + + lookup = default; + return false; + } + /// /// Provides a type that may be used to perform operations on a /// using a as a key instead of a . diff --git a/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/HashSet.cs b/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/HashSet.cs index ba6d497a29f890..621af08d72a840 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/HashSet.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/HashSet.cs @@ -365,6 +365,52 @@ public bool Remove(T item) #region AlternateLookup + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + /// The alternate type of instance for performing lookups. + /// The created lookup instance. + /// The set's comparer is not compatible with . + /// + /// The set must be using a comparer that implements with + /// and . If it doesn't, an exception will be thrown. + /// + public AlternateLookup GetAlternateLookup() + where TAlternate : allows ref struct + { + if (!AlternateLookup.IsCompatibleItem(this)) + { + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_IncompatibleComparer); + } + + return new AlternateLookup(this); + } + + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + /// The alternate type of instance for performing lookups. + /// The created lookup instance when the method returns true, or a default instance that should not be used if the method returns false. + /// true if a lookup could be created; otherwise, false. + /// + /// The set must be using a comparer that implements with + /// and . If it doesn't, the method returns false. + /// + public bool TryGetAlternateLookup(out AlternateLookup lookup) + where TAlternate : allows ref struct + { + if (AlternateLookup.IsCompatibleItem(this)) + { + lookup = new AlternateLookup(this); + return true; + } + + lookup = default; + return false; + } + /// /// Provides a type that may be used to perform operations on a /// using a instead of a . diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs index d6ade630a39864..f16e637835cf48 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs @@ -223,7 +223,7 @@ public static bool TryLookupUtf8Key( Debug.Assert(dictionary.Comparer is IAlternateEqualityComparer, string>); Dictionary.AlternateLookup> spanLookup = - dictionary.GetAlternateLookup>(); + dictionary.GetAlternateLookup>(); char[]? rentedBuffer = null; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs index e5006d23b84d4d..901cff255baa91 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs @@ -304,7 +304,7 @@ private bool TryParseNamedEnum( out T result) { #if NET9_0_OR_GREATER - Dictionary.AlternateLookup> lookup = _enumFieldInfoIndex.GetAlternateLookup>(); + Dictionary.AlternateLookup> lookup = _enumFieldInfoIndex.GetAlternateLookup>(); ReadOnlySpan rest = source; #else Dictionary lookup = _enumFieldInfoIndex;