diff --git a/src/coreclr/System.Private.CoreLib/src/System/Collections/Generic/ComparerHelpers.cs b/src/coreclr/System.Private.CoreLib/src/System/Collections/Generic/ComparerHelpers.cs index 87c07da9cdb38..954c8f90d230b 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Collections/Generic/ComparerHelpers.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Collections/Generic/ComparerHelpers.cs @@ -68,7 +68,7 @@ internal static object CreateDefaultEqualityComparer(Type type) if (type == typeof(string)) { - return new GenericEqualityComparer(); + return new StringEqualityComparer(); } else if (type.IsAssignableTo(typeof(IEquatable<>).MakeGenericType(type))) { diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/IntrinsicSupport/EqualityComparerHelpers.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/IntrinsicSupport/EqualityComparerHelpers.cs index 94592af9c8784..09a40452f432d 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/IntrinsicSupport/EqualityComparerHelpers.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/IntrinsicSupport/EqualityComparerHelpers.cs @@ -51,6 +51,11 @@ internal static unsafe bool IsEnum(RuntimeTypeHandle t) return t.ToMethodTable()->IsEnum; } + internal static unsafe bool IsString(RuntimeTypeHandle t) + { + return t.ToMethodTable()->IsString; + } + // this function utilizes the template type loader to generate new // EqualityComparer types on the fly internal static object GetComparer(RuntimeTypeHandle t) @@ -59,6 +64,11 @@ internal static object GetComparer(RuntimeTypeHandle t) RuntimeTypeHandle openComparerType = default(RuntimeTypeHandle); RuntimeTypeHandle comparerTypeArgument = default(RuntimeTypeHandle); + if (IsString(t)) + { + return new StringEqualityComparer(); + } + if (RuntimeAugments.IsNullable(t)) { RuntimeTypeHandle nullableType = RuntimeAugments.GetNullableType(t); diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Collections/Generic/EqualityComparer.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Collections/Generic/EqualityComparer.NativeAot.cs index 0b5c3d2914aa9..b7d79959332de 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Collections/Generic/EqualityComparer.NativeAot.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Collections/Generic/EqualityComparer.NativeAot.cs @@ -23,6 +23,12 @@ private static EqualityComparer Create() // This body serves as a fallback when instantiation-specific implementation is unavailable. // If that happens, the compiler ensures we generate data structures to make the fallback work // when this method is compiled. + + if (typeof(T) == typeof(string)) + { + return Unsafe.As>(new StringEqualityComparer()); + } + if (SupportsGenericIEquatableInterfaces) { return Unsafe.As>(EqualityComparerHelpers.GetComparer(typeof(T).TypeHandle)); diff --git a/src/coreclr/tools/Common/TypeSystem/IL/Stubs/ComparerIntrinsics.cs b/src/coreclr/tools/Common/TypeSystem/IL/Stubs/ComparerIntrinsics.cs index 8835a98168582..e62d825af5a11 100644 --- a/src/coreclr/tools/Common/TypeSystem/IL/Stubs/ComparerIntrinsics.cs +++ b/src/coreclr/tools/Common/TypeSystem/IL/Stubs/ComparerIntrinsics.cs @@ -75,7 +75,7 @@ private static MethodIL EmitComparerAndEqualityComparerCreateCommon(MethodDesc m /// Gets the comparer type that is suitable to compare instances of /// or null if such comparer cannot be determined at compile time. /// - private static InstantiatedType GetComparerForType(TypeDesc type, string flavor, string interfaceName) + private static TypeDesc GetComparerForType(TypeDesc type, string flavor, string interfaceName) { TypeSystemContext context = type.Context; @@ -92,6 +92,11 @@ private static InstantiatedType GetComparerForType(TypeDesc type, string flavor, .MakeInstantiatedType(type.Instantiation[0]); } + if (type.IsString && flavor == "EqualityComparer") + { + return context.SystemModule.GetKnownType("System.Collections.Generic", "StringEqualityComparer"); + } + if (type.IsEnum) { // Enums have a specialized comparer that avoids boxing diff --git a/src/coreclr/vm/corelib.h b/src/coreclr/vm/corelib.h index 0941e376d83e7..7ae4cbc710264 100644 --- a/src/coreclr/vm/corelib.h +++ b/src/coreclr/vm/corelib.h @@ -1134,6 +1134,7 @@ DEFINE_METHOD(UTF8BUFFERMARSHALER, CONVERT_TO_MANAGED, ConvertToManaged, NoSig) // Classes referenced in EqualityComparer.Default optimization +DEFINE_CLASS(STRING_EQUALITYCOMPARER, CollectionsGeneric, StringEqualityComparer) DEFINE_CLASS(ENUM_EQUALITYCOMPARER, CollectionsGeneric, EnumEqualityComparer`1) DEFINE_CLASS(NULLABLE_EQUALITYCOMPARER, CollectionsGeneric, NullableEqualityComparer`1) DEFINE_CLASS(GENERIC_EQUALITYCOMPARER, CollectionsGeneric, GenericEqualityComparer`1) diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 4b0180d2fd757..d30bfbd6dcad0 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -8848,8 +8848,7 @@ CORINFO_CLASS_HANDLE CEEInfo::getDefaultComparerClassHelper(CORINFO_CLASS_HANDLE TypeHandle elemTypeHnd(elemType); // Mirrors the logic in BCL's CompareHelpers.CreateDefaultComparer - // And in compile.cpp's SpecializeComparer - // + // We need to find the appropriate instantiation Instantiation inst(&elemTypeHnd, 1); @@ -8914,16 +8913,18 @@ CORINFO_CLASS_HANDLE CEEInfo::getDefaultEqualityComparerClassHelper(CORINFO_CLAS MODE_PREEMPTIVE; } CONTRACTL_END; - // Mirrors the logic in BCL's CompareHelpers.CreateDefaultEqualityComparer - // And in compile.cpp's SpecializeEqualityComparer + // Mirrors the logic in BCL's CompareHelpers.CreateDefaultEqualityComparer. TypeHandle elemTypeHnd(elemType); - // Mirrors the logic in BCL's CompareHelpers.CreateDefaultComparer - // And in compile.cpp's SpecializeComparer - // - // We need to find the appropriate instantiation + // We need to find the appropriate instantiation. Instantiation inst(&elemTypeHnd, 1); + // string + if (elemTypeHnd.IsString()) + { + return CORINFO_CLASS_HANDLE(CoreLibBinder::GetClass(CLASS__STRING_EQUALITYCOMPARER)); + } + // Nullable if (Nullable::IsNullableType(elemTypeHnd)) { diff --git a/src/coreclr/vm/typehandle.cpp b/src/coreclr/vm/typehandle.cpp index d53b17a27ed6c..d5fea81cb4841 100644 --- a/src/coreclr/vm/typehandle.cpp +++ b/src/coreclr/vm/typehandle.cpp @@ -84,6 +84,13 @@ BOOL TypeHandle::IsArray() const { return !IsTypeDesc() && AsMethodTable()->IsArray(); } +BOOL TypeHandle::IsString() const +{ + LIMITED_METHOD_CONTRACT; + + return !IsTypeDesc() && AsMethodTable()->IsString(); +} + BOOL TypeHandle::IsGenericVariable() const { LIMITED_METHOD_DAC_CONTRACT; diff --git a/src/coreclr/vm/typehandle.h b/src/coreclr/vm/typehandle.h index 8abeb3972d38a..9e624629b72f6 100644 --- a/src/coreclr/vm/typehandle.h +++ b/src/coreclr/vm/typehandle.h @@ -463,6 +463,9 @@ class TypeHandle // PTR BOOL IsPointer() const; + // String + BOOL IsString() const; + // True if this type *is* a formal generic type parameter or any component of it is a formal generic type parameter BOOL ContainsGenericVariables(BOOL methodOnly=FALSE) const; diff --git a/src/libraries/System.Collections.Concurrent/tests/ConcurrentDictionary/ConcurrentDictionaryTests.cs b/src/libraries/System.Collections.Concurrent/tests/ConcurrentDictionary/ConcurrentDictionaryTests.cs index 216ee55b41779..ce05af556da9c 100644 --- a/src/libraries/System.Collections.Concurrent/tests/ConcurrentDictionary/ConcurrentDictionaryTests.cs +++ b/src/libraries/System.Collections.Concurrent/tests/ConcurrentDictionary/ConcurrentDictionaryTests.cs @@ -1088,13 +1088,6 @@ public void ConcurrentWriteRead_NoTornValues() })); } - // TODO: Revise this test when EqualityComparer.Default implements IAlternateEqualityComparer, string> - [Fact] - public void GetAlternateLookup_FailsForDefaultComparer() - { - Assert.False(new ConcurrentDictionary().TryGetAlternateLookup>(out _)); - } - [Fact] public void GetAlternateLookup_FailsWhenIncompatible() { @@ -1112,26 +1105,23 @@ public void GetAlternateLookup_FailsWhenIncompatible() 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] - [InlineData(0)] - [InlineData(1)] - [InlineData(2)] - [InlineData(3)] - [InlineData(4)] - [InlineData(5)] - public void GetAlternateLookup_OperationsMatchUnderlyingDictionary(int mode) + [MemberData(nameof(Dictionary_GetAlternateLookup_OperationsMatchUnderlyingDictionary_MemberData))] + public void GetAlternateLookup_OperationsMatchUnderlyingDictionary(IEqualityComparer comparer) { // Test with a variety of comparers to ensure that the alternate lookup is consistent with the underlying dictionary - ConcurrentDictionary dictionary = 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)) - }); + ConcurrentDictionary dictionary = new(comparer); ConcurrentDictionary.AlternateLookup> lookup = dictionary.GetAlternateLookup>(); Assert.Same(dictionary, lookup.Dictionary); Assert.Same(lookup.Dictionary, lookup.Dictionary); @@ -1165,7 +1155,8 @@ public void GetAlternateLookup_OperationsMatchUnderlyingDictionary(int mode) // Ensure that case-sensitivity of the comparer is respected lookup["a".AsSpan()] = 42; - if (dictionary.Comparer.Equals(StringComparer.Ordinal) || + if (dictionary.Comparer.Equals(EqualityComparer.Default) || + dictionary.Comparer.Equals(StringComparer.Ordinal) || dictionary.Comparer.Equals(StringComparer.InvariantCulture) || dictionary.Comparer.Equals(StringComparer.CurrentCulture)) { diff --git a/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenDictionaryAlternateLookupTests.cs b/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenDictionaryAlternateLookupTests.cs index d09373c0dfba6..0b0ad5ad73475 100644 --- a/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenDictionaryAlternateLookupTests.cs +++ b/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenDictionaryAlternateLookupTests.cs @@ -12,20 +12,10 @@ public class FrozenDictionaryAlternateLookupTests [Fact] public void AlternateLookup_Empty() { - FrozenDictionary[] unsupported = + FrozenDictionary[] supported = [ - FrozenDictionary.Empty, FrozenDictionary.ToFrozenDictionary([]), FrozenDictionary.ToFrozenDictionary([], EqualityComparer.Default), - ]; - foreach (FrozenDictionary frozen in unsupported) - { - Assert.Throws(() => frozen.GetAlternateLookup>()); - Assert.False(frozen.TryGetAlternateLookup>(out _)); - } - - FrozenDictionary[] supported = - [ FrozenDictionary.ToFrozenDictionary([], StringComparer.Ordinal), FrozenDictionary.ToFrozenDictionary([], StringComparer.OrdinalIgnoreCase), ]; @@ -39,7 +29,7 @@ public void AlternateLookup_Empty() [Fact] public void UnsupportedComparer_ThrowsOrReturnsFalse() { - FrozenDictionary frozen = new Dictionary { ["a"] = 1, ["b"] = 2 }.ToFrozenDictionary(); + FrozenDictionary frozen = new Dictionary { ['a'] = 1, ['b'] = 2 }.ToFrozenDictionary(); Assert.Throws(() => frozen.GetAlternateLookup>()); Assert.False(frozen.TryGetAlternateLookup>(out _)); } diff --git a/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenSetAlternateLookupTests.cs b/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenSetAlternateLookupTests.cs index 527dc327b37d5..559864e5cc84a 100644 --- a/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenSetAlternateLookupTests.cs +++ b/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenSetAlternateLookupTests.cs @@ -12,9 +12,9 @@ public class FrozenSetAlternateLookupTests [Fact] public void AlternateLookup_Empty() { - Assert.False(FrozenSet.Empty.TryGetAlternateLookup>(out _)); + Assert.True(FrozenSet.Empty.TryGetAlternateLookup>(out _)); - foreach (StringComparer comparer in new[] { StringComparer.Ordinal, StringComparer.OrdinalIgnoreCase }) + foreach (IEqualityComparer comparer in new IEqualityComparer[] { null, EqualityComparer.Default, StringComparer.Ordinal, StringComparer.OrdinalIgnoreCase }) { FrozenSet.AlternateLookup> lookup = FrozenSet.ToFrozenSet([], comparer).GetAlternateLookup>(); Assert.False(lookup.Contains("anything".AsSpan())); @@ -24,7 +24,7 @@ public void AlternateLookup_Empty() [Fact] public void UnsupportedComparer() { - FrozenSet frozen = FrozenSet.ToFrozenSet(["a", "b"]); + FrozenSet frozen = FrozenSet.ToFrozenSet(['a', 'b']); Assert.Throws(() => frozen.GetAlternateLookup>()); Assert.False(frozen.TryGetAlternateLookup>(out _)); } diff --git a/src/libraries/System.Collections/tests/Generic/CollectionExtensionsTests.cs b/src/libraries/System.Collections/tests/Generic/CollectionExtensionsTests.cs index 50e7253deadd5..b43f5affdaee5 100644 --- a/src/libraries/System.Collections/tests/Generic/CollectionExtensionsTests.cs +++ b/src/libraries/System.Collections/tests/Generic/CollectionExtensionsTests.cs @@ -151,15 +151,6 @@ public void GetAlternateLookup_ThrowsWhenNull() AssertExtensions.Throws("set", () => CollectionExtensions.TryGetAlternateLookup((HashSet)null, out _)); } - // TODO https://github.com/dotnet/runtime/issues/102906: - // Revise this test when EqualityComparer.Default implements IAlternateEqualityComparer, string> - [Fact] - public void GetAlternateLookup_FailsForDefaultComparer() - { - Assert.False(new Dictionary().TryGetAlternateLookup>(out _)); - Assert.False(new HashSet().TryGetAlternateLookup>(out _)); - } - [Fact] public void GetAlternateLookup_FailsWhenIncompatible() { @@ -189,26 +180,23 @@ public void GetAlternateLookup_FailsWhenIncompatible() 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] - [InlineData(0)] - [InlineData(1)] - [InlineData(2)] - [InlineData(3)] - [InlineData(4)] - [InlineData(5)] - public void Dictionary_GetAlternateLookup_OperationsMatchUnderlyingDictionary(int mode) + [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(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)) - }); + Dictionary dictionary = new(comparer); Dictionary.AlternateLookup> lookup = dictionary.GetAlternateLookup>(); Assert.Same(dictionary, lookup.Dictionary); Assert.Same(lookup.Dictionary, lookup.Dictionary); @@ -241,7 +229,8 @@ public void Dictionary_GetAlternateLookup_OperationsMatchUnderlyingDictionary(in // Ensure that case-sensitivity of the comparer is respected lookup["a".AsSpan()] = 42; - if (dictionary.Comparer.Equals(StringComparer.Ordinal) || + if (dictionary.Comparer.Equals(EqualityComparer.Default) || + dictionary.Comparer.Equals(StringComparer.Ordinal) || dictionary.Comparer.Equals(StringComparer.InvariantCulture) || dictionary.Comparer.Equals(StringComparer.CurrentCulture)) { 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 6fe06dd432e74..f0234d4ba42e9 100644 --- a/src/libraries/System.Collections/tests/Generic/Dictionary/HashCollisionScenarios/OutOfBoundsRegression.cs +++ b/src/libraries/System.Collections/tests/Generic/Dictionary/HashCollisionScenarios/OutOfBoundsRegression.cs @@ -18,7 +18,7 @@ public class InternalHashCodeTests_Dictionary_NullComparer : InternalHashCodeTes protected override bool ContainsKey(Dictionary collection, string key) => collection.ContainsKey(key); protected override IEqualityComparer GetComparer(Dictionary collection) => collection.Comparer; - protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedDefaultComparerType; + protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedOrdinalComparerType; protected override IEqualityComparer ExpectedPublicComparerBeforeCollisionThreshold => EqualityComparer.Default; protected override Type ExpectedInternalComparerTypeAfterCollisionThreshold => randomizedOrdinalComparerType; @@ -60,7 +60,7 @@ public class InternalHashCodeTests_Dictionary_DefaultComparer : InternalHashCode protected override bool ContainsKey(Dictionary collection, string key) => collection.ContainsKey(key); protected override IEqualityComparer GetComparer(Dictionary collection) => collection.Comparer; - protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedDefaultComparerType; + protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedOrdinalComparerType; protected override IEqualityComparer ExpectedPublicComparerBeforeCollisionThreshold => EqualityComparer.Default; protected override Type ExpectedInternalComparerTypeAfterCollisionThreshold => randomizedOrdinalComparerType; } @@ -124,7 +124,7 @@ public class InternalHashCodeTests_HashSet_NullComparer : InternalHashCodeTests< protected override bool ContainsKey(HashSet collection, string key) => collection.Contains(key); protected override IEqualityComparer GetComparer(HashSet collection) => collection.Comparer; - protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedDefaultComparerType; + protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedOrdinalComparerType; protected override IEqualityComparer ExpectedPublicComparerBeforeCollisionThreshold => EqualityComparer.Default; protected override Type ExpectedInternalComparerTypeAfterCollisionThreshold => randomizedOrdinalComparerType; } @@ -136,7 +136,7 @@ public class InternalHashCodeTests_HashSet_DefaultComparer : InternalHashCodeTes protected override bool ContainsKey(HashSet collection, string key) => collection.Contains(key); protected override IEqualityComparer GetComparer(HashSet collection) => collection.Comparer; - protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedDefaultComparerType; + protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedOrdinalComparerType; protected override IEqualityComparer ExpectedPublicComparerBeforeCollisionThreshold => EqualityComparer.Default; protected override Type ExpectedInternalComparerTypeAfterCollisionThreshold => randomizedOrdinalComparerType; } @@ -186,7 +186,7 @@ public class InternalHashCodeTests_OrderedDictionary_NullComparer : InternalHash protected override bool ContainsKey(OrderedDictionary collection, string key) => collection.ContainsKey(key); protected override IEqualityComparer GetComparer(OrderedDictionary collection) => collection.Comparer; - protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedDefaultComparerType; + protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedOrdinalComparerType; protected override IEqualityComparer ExpectedPublicComparerBeforeCollisionThreshold => EqualityComparer.Default; protected override Type ExpectedInternalComparerTypeAfterCollisionThreshold => EqualityComparer.Default.GetType(); } @@ -198,7 +198,7 @@ public class InternalHashCodeTests_OrderedDictionary_DefaultComparer : InternalH protected override bool ContainsKey(OrderedDictionary collection, string key) => collection.ContainsKey(key); protected override IEqualityComparer GetComparer(OrderedDictionary collection) => collection.Comparer; - protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedDefaultComparerType; + protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedOrdinalComparerType; protected override IEqualityComparer ExpectedPublicComparerBeforeCollisionThreshold => EqualityComparer.Default; protected override Type ExpectedInternalComparerTypeAfterCollisionThreshold => EqualityComparer.Default.GetType(); } @@ -242,7 +242,6 @@ public class InternalHashCodeTests_OrderedDictionary_LinguisticComparer : Intern public abstract class InternalHashCodeTests { - protected static Type nonRandomizedDefaultComparerType = typeof(object).Assembly.GetType("System.Collections.Generic.NonRandomizedStringEqualityComparer+DefaultComparer", throwOnError: true); protected static Type nonRandomizedOrdinalComparerType = typeof(object).Assembly.GetType("System.Collections.Generic.NonRandomizedStringEqualityComparer+OrdinalComparer", throwOnError: true); protected static Type nonRandomizedOrdinalIgnoreCaseComparerType = typeof(object).Assembly.GetType("System.Collections.Generic.NonRandomizedStringEqualityComparer+OrdinalIgnoreCaseComparer", throwOnError: true); protected static Type randomizedOrdinalComparerType = typeof(object).Assembly.GetType("System.Collections.Generic.RandomizedStringEqualityComparer+OrdinalComparer", throwOnError: true); diff --git a/src/libraries/System.Formats.Nrbf/tests/ReadAnythingTests.cs b/src/libraries/System.Formats.Nrbf/tests/ReadAnythingTests.cs index fd84071fac8e0..66cbab818dbba 100644 --- a/src/libraries/System.Formats.Nrbf/tests/ReadAnythingTests.cs +++ b/src/libraries/System.Formats.Nrbf/tests/ReadAnythingTests.cs @@ -13,7 +13,7 @@ public class ReadAnythingTests : ReadTests [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework, "Fails with ValueTuple is not marked as serializable, but only in this repo")] public void UserCanReadAnyValidInputAndCheckTypesUsingStronglyTypedTypeInstances() { - Dictionary input = new() + Dictionary input = new(new CustomStringComparer()) { { "exception", new Exception("test") }, { "struct", new ValueTuple(true, 123) }, @@ -269,6 +269,13 @@ static void VerifyDictionary(ClassRecord record) } } + [Serializable] + public class CustomStringComparer : IEqualityComparer + { + public bool Equals(string? x, string? y) => x == y; + public int GetHashCode(string x) => x?.GetHashCode() ?? 0; + } + [Serializable] public class NonSystemPoint : IComparable, IEquatable { diff --git a/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/EqualityComparer.cs b/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/EqualityComparer.cs index aedbfa953bf88..43b2612505680 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/EqualityComparer.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/EqualityComparer.cs @@ -262,4 +262,59 @@ public override bool Equals([NotNullWhen(true)] object? obj) => public override int GetHashCode() => GetType().GetHashCode(); } + + // This class exists to be EqualityComparer.Default. It can't just use the GenericEqualityComparer, + // as it needs to also implement IAlternateEqualityComparer, string>, and it can't be + // StringComparer.Ordinal, as that doesn't derive from the required abstract EqualityComparer base class. + [Serializable] + internal sealed partial class StringEqualityComparer : + EqualityComparer, + IAlternateEqualityComparer, string>, + ISerializable + { + void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) + { + // This type is added as an internal implementation detail in .NET 9. Even though as of .NET 9 BinaryFormatter has been + // deprecated, for back compat we still need to support serializing this type, especially when EqualityComparer.Default + // is used as part of a collection, like Dictionary. + // + // BinaryFormatter treats types in the core library as being special, in that it doesn't include the assembly as part of the + // serialized data, and then on deserialization it assumes the type is in mscorlib. We could make the type public and type forward + // it from the mscorlib shim, which would enable roundtripping on .NET 9+, but because this type doesn't exist downlevel, it would + // break serializing on .NET 9+ and deserializing downlevel. Therefore, we need to serialize as something that exists downlevel. + + // We could serialize as OrdinalComparer, which does exist downlevel, and which has the nice property that it also implements + // IAlternateEqualityComparer, string>, which means serializing an instance on .NET 9+ and deserializing it + // on .NET 9+ would continue to support span-based lookups. However, OrdinalComparer is not an EqualityComparer, which + // means the type's public ancestry would not be retained, which could lead to strange casting-related errors, including downlevel. + + // Instead, we can serialize as a GenericEqualityComparer. This exists downlevel and also derives from EqualityComparer, + // but doesn't implement IAlternateEqualityComparer, string>. This means that upon deserializing on .NET 9+, + // the comparer loses its ability to handle span-based lookups. As BinaryFormatter is deprecated on .NET 9+, this is a readonable tradeoff. + + info.SetType(typeof(GenericEqualityComparer)); + } + + public override bool Equals(string? x, string? y) => string.Equals(x, y); + + public override int GetHashCode([DisallowNull] string obj) => obj?.GetHashCode() ?? 0; + + public bool Equals(ReadOnlySpan span, string target) + { + // See explanation in OrdinalComparer.Equals. + if (span.IsEmpty && target is null) + { + return false; + } + + return span.SequenceEqual(target); + } + + public int GetHashCode(ReadOnlySpan span) => string.GetHashCode(span); + + public string Create(ReadOnlySpan span) => span.ToString(); + + public override bool Equals(object? obj) => obj is StringEqualityComparer; + public override int GetHashCode() => GetType().GetHashCode(); + } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/NonRandomizedStringEqualityComparer.cs b/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/NonRandomizedStringEqualityComparer.cs index b3a5d8d801313..9bc99f7ef4c82 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/NonRandomizedStringEqualityComparer.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/NonRandomizedStringEqualityComparer.cs @@ -20,7 +20,7 @@ public class NonRandomizedStringEqualityComparer : IEqualityComparer, I // that was passed in to the ctor. The caller chooses one of these singletons so that the // GetUnderlyingEqualityComparer method can return the correct value. - private static readonly NonRandomizedStringEqualityComparer WrappedAroundDefaultComparer = new DefaultComparer(EqualityComparer.Default); + private static readonly NonRandomizedStringEqualityComparer WrappedAroundDefaultComparer = new OrdinalComparer(EqualityComparer.Default); private static readonly NonRandomizedStringEqualityComparer WrappedAroundStringComparerOrdinal = new OrdinalComparer(StringComparer.Ordinal); private static readonly NonRandomizedStringEqualityComparer WrappedAroundStringComparerOrdinalIgnoreCase = new OrdinalIgnoreCaseComparer(StringComparer.OrdinalIgnoreCase); @@ -74,27 +74,6 @@ void ISerializable.GetObjectData(SerializationInfo info, StreamingContext contex info.SetType(typeof(GenericEqualityComparer)); } - // TODO https://github.com/dotnet/runtime/issues/102906: - // This custom class exists because EqualityComparer.Default doesn't implement IAlternateEqualityComparer, string>. - // If OrdinalComparer were used, then a dictionary created with a null/Default comparer would be using a comparer that does - // implement IAlternateEqualityComparer, string>, but only until it hits a collision and switches to the randomized comparer. - // Once EqualityComparer.Default implements IAlternateEqualityComparer, string>, we can remove this class, and change - // WrappedAroundDefaultComparer to be an instance of OrdinalComparer. - private sealed class DefaultComparer : NonRandomizedStringEqualityComparer - { - internal DefaultComparer(IEqualityComparer wrappedComparer) : base(wrappedComparer) - { - } - - public override bool Equals(string? x, string? y) => string.Equals(x, y); - - public override int GetHashCode(string? obj) - { - Debug.Assert(obj != null, "This implementation is only called from first-party collection types that guarantee non-null parameters."); - return obj.GetNonRandomizedHashCode(); - } - } - private sealed class OrdinalComparer : NonRandomizedStringEqualityComparer, IAlternateEqualityComparer, string?> { internal OrdinalComparer(IEqualityComparer wrappedComparer) : base(wrappedComparer) diff --git a/src/libraries/System.Private.CoreLib/src/System/StringComparer.cs b/src/libraries/System.Private.CoreLib/src/System/StringComparer.cs index d6f0f02c0f025..287ede3cd97c2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/StringComparer.cs +++ b/src/libraries/System.Private.CoreLib/src/System/StringComparer.cs @@ -88,10 +88,12 @@ public static bool IsWellKnownOrdinalComparer(IEqualityComparer? compar { case StringComparer stringComparer: return stringComparer.IsWellKnownOrdinalComparerCore(out ignoreCase); - case GenericEqualityComparer: - // special-case EqualityComparer.Default, which is Ordinal-equivalent + + case StringEqualityComparer: // EqualityComparer.Default + case GenericEqualityComparer: // EqualityComparer.Default serialized ignoreCase = false; return true; + default: // unknown comparer ignoreCase = default; diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Legacy/BinaryFormatterTestData.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Legacy/BinaryFormatterTestData.cs index 286f0277f427d..93da58616cc04 100644 --- a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Legacy/BinaryFormatterTestData.cs +++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Legacy/BinaryFormatterTestData.cs @@ -78,6 +78,7 @@ public partial class BinaryFormatterTests yield return (EqualityComparer.Default, new TypeSerializableValue[] { new("AAEAAAD/////AQAAAAAAAAAEAQAAANUBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuT2JqZWN0RXF1YWxpdHlDb21wYXJlcmAxW1tCaW5hcnlGb3JtYXRUZXN0cy5Gb3JtYXR0ZXJUZXN0cy5VSW50NjRFbnVtLCBTeXN0ZW0uUmVzb3VyY2VzLkV4dGVuc2lvbnMuQmluYXJ5Rm9ybWF0LlRlc3RzLCBWZXJzaW9uPTkuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Y2M3YjEzZmZjZDJkZGQ1MV1dAAAAAAs=", TargetFrameworkMoniker.netcoreapp20), new("AAEAAAD/////AQAAAAAAAAAEAQAAANUBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuT2JqZWN0RXF1YWxpdHlDb21wYXJlcmAxW1tCaW5hcnlGb3JtYXRUZXN0cy5Gb3JtYXR0ZXJUZXN0cy5VSW50NjRFbnVtLCBTeXN0ZW0uUmVzb3VyY2VzLkV4dGVuc2lvbnMuQmluYXJ5Rm9ybWF0LlRlc3RzLCBWZXJzaW9uPTkuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Y2M3YjEzZmZjZDJkZGQ1MV1dAAAAAAs=", TargetFrameworkMoniker.netfx461) }); yield return (EqualityComparer.Default, new TypeSerializableValue[] { new("AAEAAAD/////AQAAAAAAAAAEAQAAANQBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuT2JqZWN0RXF1YWxpdHlDb21wYXJlcmAxW1tCaW5hcnlGb3JtYXRUZXN0cy5Gb3JtYXR0ZXJUZXN0cy5TQnl0ZUVudW0sIFN5c3RlbS5SZXNvdXJjZXMuRXh0ZW5zaW9ucy5CaW5hcnlGb3JtYXQuVGVzdHMsIFZlcnNpb249OS4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1jYzdiMTNmZmNkMmRkZDUxXV0AAAAACw==", TargetFrameworkMoniker.netcoreapp20), new("AAEAAAD/////AQAAAAAAAAAEAQAAANQBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuT2JqZWN0RXF1YWxpdHlDb21wYXJlcmAxW1tCaW5hcnlGb3JtYXRUZXN0cy5Gb3JtYXR0ZXJUZXN0cy5TQnl0ZUVudW0sIFN5c3RlbS5SZXNvdXJjZXMuRXh0ZW5zaW9ucy5CaW5hcnlGb3JtYXQuVGVzdHMsIFZlcnNpb249OS4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1jYzdiMTNmZmNkMmRkZDUxXV0AAAAACw==", TargetFrameworkMoniker.netfx461) }); yield return (EqualityComparer.Default, new TypeSerializableValue[] { new("AAEAAAD/////AQAAAAAAAAAEAQAAANQBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuT2JqZWN0RXF1YWxpdHlDb21wYXJlcmAxW1tCaW5hcnlGb3JtYXRUZXN0cy5Gb3JtYXR0ZXJUZXN0cy5JbnQxNkVudW0sIFN5c3RlbS5SZXNvdXJjZXMuRXh0ZW5zaW9ucy5CaW5hcnlGb3JtYXQuVGVzdHMsIFZlcnNpb249OS4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1jYzdiMTNmZmNkMmRkZDUxXV0AAAAACw==", TargetFrameworkMoniker.netcoreapp20), new("AAEAAAD/////AQAAAAAAAAAEAQAAANQBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuT2JqZWN0RXF1YWxpdHlDb21wYXJlcmAxW1tCaW5hcnlGb3JtYXRUZXN0cy5Gb3JtYXR0ZXJUZXN0cy5JbnQxNkVudW0sIFN5c3RlbS5SZXNvdXJjZXMuRXh0ZW5zaW9ucy5CaW5hcnlGb3JtYXQuVGVzdHMsIFZlcnNpb249OS4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1jYzdiMTNmZmNkMmRkZDUxXV0AAAAACw==", TargetFrameworkMoniker.netfx461) }); + yield return (EqualityComparer.Default, new TypeSerializableValue[] { new("AAEAAAD/////AQAAAAAAAAAEAQAAAJIBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuR2VuZXJpY0VxdWFsaXR5Q29tcGFyZXJgMVtbU3lzdGVtLlN0cmluZywgbXNjb3JsaWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0AAAAACw==", TargetFrameworkMoniker.netcoreapp20), new("AAEAAAD/////AQAAAAAAAAAEAQAAAJIBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuR2VuZXJpY0VxdWFsaXR5Q29tcGFyZXJgMVtbU3lzdGVtLlN0cmluZywgbXNjb3JsaWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0AAAAACw==", TargetFrameworkMoniker.netfx461) }); } /// @@ -667,7 +668,6 @@ public partial class BinaryFormatterTests // Nullable equality comparer roundtrips as opposed to other equality comparers which serialize to ObjectEqualityComparer yield return (EqualityComparer.Default, new TypeSerializableValue[] { new("AAEAAAD/////AQAAAAAAAAAEAQAAAJEBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuR2VuZXJpY0VxdWFsaXR5Q29tcGFyZXJgMVtbU3lzdGVtLkludDMyLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQAAAAAL", TargetFrameworkMoniker.netcoreapp20), new("AAEAAAD/////AQAAAAAAAAAEAQAAAJEBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuR2VuZXJpY0VxdWFsaXR5Q29tcGFyZXJgMVtbU3lzdGVtLkludDMyLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQAAAAAL", TargetFrameworkMoniker.netfx461) }); yield return (EqualityComparer.Default, new TypeSerializableValue[] { new("AAEAAAD/////AQAAAAAAAAAEAQAAAJEBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuR2VuZXJpY0VxdWFsaXR5Q29tcGFyZXJgMVtbU3lzdGVtLkludDY0LCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQAAAAAL", TargetFrameworkMoniker.netcoreapp20), new("AAEAAAD/////AQAAAAAAAAAEAQAAAJEBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuR2VuZXJpY0VxdWFsaXR5Q29tcGFyZXJgMVtbU3lzdGVtLkludDY0LCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQAAAAAL", TargetFrameworkMoniker.netfx461) }); - yield return (EqualityComparer.Default, new TypeSerializableValue[] { new("AAEAAAD/////AQAAAAAAAAAEAQAAAJIBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuR2VuZXJpY0VxdWFsaXR5Q29tcGFyZXJgMVtbU3lzdGVtLlN0cmluZywgbXNjb3JsaWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0AAAAACw==", TargetFrameworkMoniker.netcoreapp20), new("AAEAAAD/////AQAAAAAAAAAEAQAAAJIBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuR2VuZXJpY0VxdWFsaXR5Q29tcGFyZXJgMVtbU3lzdGVtLlN0cmluZywgbXNjb3JsaWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0AAAAACw==", TargetFrameworkMoniker.netfx461) }); yield return (EqualityComparer.Default, new TypeSerializableValue[] { new("AAEAAAD/////AQAAAAAAAAAEAQAAAJEBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuT2JqZWN0RXF1YWxpdHlDb21wYXJlcmAxW1tTeXN0ZW0uT2JqZWN0LCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQAAAAAL", TargetFrameworkMoniker.netcoreapp20), new("AAEAAAD/////AQAAAAAAAAAEAQAAAJEBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuT2JqZWN0RXF1YWxpdHlDb21wYXJlcmAxW1tTeXN0ZW0uT2JqZWN0LCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQAAAAAL", TargetFrameworkMoniker.netfx461) }); yield return (EqualityComparer.Default, new TypeSerializableValue[] { new("AAEAAAD/////AQAAAAAAAAAEAQAAAJIBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuTnVsbGFibGVFcXVhbGl0eUNvbXBhcmVyYDFbW1N5c3RlbS5JbnQzMiwgbXNjb3JsaWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0AAAAACw==", TargetFrameworkMoniker.netcoreapp20), new("AAEAAAD/////AQAAAAAAAAAEAQAAAJIBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuTnVsbGFibGVFcXVhbGl0eUNvbXBhcmVyYDFbW1N5c3RlbS5JbnQzMiwgbXNjb3JsaWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0AAAAACw==", TargetFrameworkMoniker.netfx461) }); yield return (EqualityComparer.Default, new TypeSerializableValue[] { new("AAEAAAD/////AQAAAAAAAAAEAQAAAJMBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuTnVsbGFibGVFcXVhbGl0eUNvbXBhcmVyYDFbW1N5c3RlbS5Eb3VibGUsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV1dAAAAAAs=", TargetFrameworkMoniker.netcoreapp20), new("AAEAAAD/////AQAAAAAAAAAEAQAAAJMBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuTnVsbGFibGVFcXVhbGl0eUNvbXBhcmVyYDFbW1N5c3RlbS5Eb3VibGUsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV1dAAAAAAs=", TargetFrameworkMoniker.netfx461) }); diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Legacy/BinaryFormatterTests.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Legacy/BinaryFormatterTests.cs index 8f99d4437ba36..fbd0544153a76 100644 --- a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Legacy/BinaryFormatterTests.cs +++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Legacy/BinaryFormatterTests.cs @@ -182,7 +182,7 @@ private static void ValidateEqualityComparer(object obj) { Type objType = obj.GetType(); Assert.True(objType.IsGenericType, $"Type `{objType.FullName}` must be generic."); - Assert.Equal("System.Collections.Generic.ObjectEqualityComparer`1", objType.GetGenericTypeDefinition().FullName); + Assert.True(objType.GetGenericTypeDefinition().FullName is "System.Collections.Generic.ObjectEqualityComparer`1" or "System.Collections.Generic.GenericEqualityComparer`1"); Assert.Equal(obj.GetType().GetGenericArguments()[0], objType.GetGenericArguments()[0]); } diff --git a/src/libraries/System.Resources.Extensions/tests/TestData.resources b/src/libraries/System.Resources.Extensions/tests/TestData.resources index 10efbf9fd4044..0f45dd390ac64 100644 Binary files a/src/libraries/System.Resources.Extensions/tests/TestData.resources and b/src/libraries/System.Resources.Extensions/tests/TestData.resources differ diff --git a/src/libraries/System.Runtime.Serialization.Formatters/tests/BinaryFormatterTestData.cs b/src/libraries/System.Runtime.Serialization.Formatters/tests/BinaryFormatterTestData.cs index d3e06e69b4392..c2d95437fded5 100644 --- a/src/libraries/System.Runtime.Serialization.Formatters/tests/BinaryFormatterTestData.cs +++ b/src/libraries/System.Runtime.Serialization.Formatters/tests/BinaryFormatterTestData.cs @@ -93,6 +93,7 @@ public static IEnumerable SerializableEqualityComparers_MemberData() yield return new object[] { EqualityComparer.Default, new TypeSerializableValue[] { new TypeSerializableValue("AAEAAAD/////AQAAAAAAAAAEAQAAAOEBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuT2JqZWN0RXF1YWxpdHlDb21wYXJlcmAxW1tTeXN0ZW0uUnVudGltZS5TZXJpYWxpemF0aW9uLkZvcm1hdHRlcnMuVGVzdHMuVUludDY0RW51bSwgU3lzdGVtLlJ1bnRpbWUuU2VyaWFsaXphdGlvbi5Gb3JtYXR0ZXJzLlRlc3RzLCBWZXJzaW9uPTQuMC4zLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49OWQ3N2NjN2FkMzliNjhlYl1dAAAAAAs=", TargetFrameworkMoniker.netcoreapp20), new TypeSerializableValue("AAEAAAD/////AQAAAAAAAAAEAQAAAOEBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuT2JqZWN0RXF1YWxpdHlDb21wYXJlcmAxW1tTeXN0ZW0uUnVudGltZS5TZXJpYWxpemF0aW9uLkZvcm1hdHRlcnMuVGVzdHMuVUludDY0RW51bSwgU3lzdGVtLlJ1bnRpbWUuU2VyaWFsaXphdGlvbi5Gb3JtYXR0ZXJzLlRlc3RzLCBWZXJzaW9uPTQuMC4zLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49OWQ3N2NjN2FkMzliNjhlYl1dAAAAAAs=", TargetFrameworkMoniker.netfx461) } }; yield return new object[] { EqualityComparer.Default, new TypeSerializableValue[] { new TypeSerializableValue("AAEAAAD/////AQAAAAAAAAAEAQAAAOABU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuT2JqZWN0RXF1YWxpdHlDb21wYXJlcmAxW1tTeXN0ZW0uUnVudGltZS5TZXJpYWxpemF0aW9uLkZvcm1hdHRlcnMuVGVzdHMuU0J5dGVFbnVtLCBTeXN0ZW0uUnVudGltZS5TZXJpYWxpemF0aW9uLkZvcm1hdHRlcnMuVGVzdHMsIFZlcnNpb249NC4wLjMuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj05ZDc3Y2M3YWQzOWI2OGViXV0AAAAACw==", TargetFrameworkMoniker.netcoreapp20), new TypeSerializableValue("AAEAAAD/////AQAAAAAAAAAEAQAAAOABU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuT2JqZWN0RXF1YWxpdHlDb21wYXJlcmAxW1tTeXN0ZW0uUnVudGltZS5TZXJpYWxpemF0aW9uLkZvcm1hdHRlcnMuVGVzdHMuU0J5dGVFbnVtLCBTeXN0ZW0uUnVudGltZS5TZXJpYWxpemF0aW9uLkZvcm1hdHRlcnMuVGVzdHMsIFZlcnNpb249NC4wLjMuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj05ZDc3Y2M3YWQzOWI2OGViXV0AAAAACw==", TargetFrameworkMoniker.netfx461) } }; yield return new object[] { EqualityComparer.Default, new TypeSerializableValue[] { new TypeSerializableValue("AAEAAAD/////AQAAAAAAAAAEAQAAAOABU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuT2JqZWN0RXF1YWxpdHlDb21wYXJlcmAxW1tTeXN0ZW0uUnVudGltZS5TZXJpYWxpemF0aW9uLkZvcm1hdHRlcnMuVGVzdHMuSW50MTZFbnVtLCBTeXN0ZW0uUnVudGltZS5TZXJpYWxpemF0aW9uLkZvcm1hdHRlcnMuVGVzdHMsIFZlcnNpb249NC4wLjMuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj05ZDc3Y2M3YWQzOWI2OGViXV0AAAAACw==", TargetFrameworkMoniker.netcoreapp20), new TypeSerializableValue("AAEAAAD/////AQAAAAAAAAAEAQAAAOABU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuT2JqZWN0RXF1YWxpdHlDb21wYXJlcmAxW1tTeXN0ZW0uUnVudGltZS5TZXJpYWxpemF0aW9uLkZvcm1hdHRlcnMuVGVzdHMuSW50MTZFbnVtLCBTeXN0ZW0uUnVudGltZS5TZXJpYWxpemF0aW9uLkZvcm1hdHRlcnMuVGVzdHMsIFZlcnNpb249NC4wLjMuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj05ZDc3Y2M3YWQzOWI2OGViXV0AAAAACw==", TargetFrameworkMoniker.netfx461) } }; + yield return new object[] { EqualityComparer.Default, new TypeSerializableValue[] { new TypeSerializableValue("AAEAAAD/////AQAAAAAAAAAEAQAAAJIBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuR2VuZXJpY0VxdWFsaXR5Q29tcGFyZXJgMVtbU3lzdGVtLlN0cmluZywgbXNjb3JsaWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0AAAAACw==", TargetFrameworkMoniker.netcoreapp20), new TypeSerializableValue("AAEAAAD/////AQAAAAAAAAAEAQAAAJIBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuR2VuZXJpY0VxdWFsaXR5Q29tcGFyZXJgMVtbU3lzdGVtLlN0cmluZywgbXNjb3JsaWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0AAAAACw==", TargetFrameworkMoniker.netfx461) } }; } /// @@ -820,7 +821,6 @@ public static IEnumerable SerializableObjects() // Nullable equality comparer roundtrips as opposed to other equality comparers which serialize to ObjectEqualityComparer yield return new object[] { EqualityComparer.Default, new TypeSerializableValue[] { new TypeSerializableValue("AAEAAAD/////AQAAAAAAAAAEAQAAAJEBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuR2VuZXJpY0VxdWFsaXR5Q29tcGFyZXJgMVtbU3lzdGVtLkludDMyLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQAAAAAL", TargetFrameworkMoniker.netcoreapp20), new TypeSerializableValue("AAEAAAD/////AQAAAAAAAAAEAQAAAJEBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuR2VuZXJpY0VxdWFsaXR5Q29tcGFyZXJgMVtbU3lzdGVtLkludDMyLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQAAAAAL", TargetFrameworkMoniker.netfx461) } }; yield return new object[] { EqualityComparer.Default, new TypeSerializableValue[] { new TypeSerializableValue("AAEAAAD/////AQAAAAAAAAAEAQAAAJEBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuR2VuZXJpY0VxdWFsaXR5Q29tcGFyZXJgMVtbU3lzdGVtLkludDY0LCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQAAAAAL", TargetFrameworkMoniker.netcoreapp20), new TypeSerializableValue("AAEAAAD/////AQAAAAAAAAAEAQAAAJEBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuR2VuZXJpY0VxdWFsaXR5Q29tcGFyZXJgMVtbU3lzdGVtLkludDY0LCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQAAAAAL", TargetFrameworkMoniker.netfx461) } }; - yield return new object[] { EqualityComparer.Default, new TypeSerializableValue[] { new TypeSerializableValue("AAEAAAD/////AQAAAAAAAAAEAQAAAJIBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuR2VuZXJpY0VxdWFsaXR5Q29tcGFyZXJgMVtbU3lzdGVtLlN0cmluZywgbXNjb3JsaWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0AAAAACw==", TargetFrameworkMoniker.netcoreapp20), new TypeSerializableValue("AAEAAAD/////AQAAAAAAAAAEAQAAAJIBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuR2VuZXJpY0VxdWFsaXR5Q29tcGFyZXJgMVtbU3lzdGVtLlN0cmluZywgbXNjb3JsaWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0AAAAACw==", TargetFrameworkMoniker.netfx461) } }; yield return new object[] { EqualityComparer.Default, new TypeSerializableValue[] { new TypeSerializableValue("AAEAAAD/////AQAAAAAAAAAEAQAAAJEBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuT2JqZWN0RXF1YWxpdHlDb21wYXJlcmAxW1tTeXN0ZW0uT2JqZWN0LCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQAAAAAL", TargetFrameworkMoniker.netcoreapp20), new TypeSerializableValue("AAEAAAD/////AQAAAAAAAAAEAQAAAJEBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuT2JqZWN0RXF1YWxpdHlDb21wYXJlcmAxW1tTeXN0ZW0uT2JqZWN0LCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQAAAAAL", TargetFrameworkMoniker.netfx461) } }; yield return new object[] { EqualityComparer.Default, new TypeSerializableValue[] { new TypeSerializableValue("AAEAAAD/////AQAAAAAAAAAEAQAAAJIBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuTnVsbGFibGVFcXVhbGl0eUNvbXBhcmVyYDFbW1N5c3RlbS5JbnQzMiwgbXNjb3JsaWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0AAAAACw==", TargetFrameworkMoniker.netcoreapp20), new TypeSerializableValue("AAEAAAD/////AQAAAAAAAAAEAQAAAJIBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuTnVsbGFibGVFcXVhbGl0eUNvbXBhcmVyYDFbW1N5c3RlbS5JbnQzMiwgbXNjb3JsaWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0AAAAACw==", TargetFrameworkMoniker.netfx461) } }; yield return new object[] { EqualityComparer.Default, new TypeSerializableValue[] { new TypeSerializableValue("AAEAAAD/////AQAAAAAAAAAEAQAAAJMBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuTnVsbGFibGVFcXVhbGl0eUNvbXBhcmVyYDFbW1N5c3RlbS5Eb3VibGUsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV1dAAAAAAs=", TargetFrameworkMoniker.netcoreapp20), new TypeSerializableValue("AAEAAAD/////AQAAAAAAAAAEAQAAAJMBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuTnVsbGFibGVFcXVhbGl0eUNvbXBhcmVyYDFbW1N5c3RlbS5Eb3VibGUsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV1dAAAAAAs=", TargetFrameworkMoniker.netfx461) } }; diff --git a/src/libraries/System.Runtime.Serialization.Formatters/tests/BinaryFormatterTests.cs b/src/libraries/System.Runtime.Serialization.Formatters/tests/BinaryFormatterTests.cs index 82e7f045e384d..4587f3e47b1fa 100644 --- a/src/libraries/System.Runtime.Serialization.Formatters/tests/BinaryFormatterTests.cs +++ b/src/libraries/System.Runtime.Serialization.Formatters/tests/BinaryFormatterTests.cs @@ -540,7 +540,7 @@ private static void ValidateEqualityComparer(object obj) { Type objType = obj.GetType(); Assert.True(objType.IsGenericType, $"Type `{objType.FullName}` must be generic."); - Assert.Equal("System.Collections.Generic.ObjectEqualityComparer`1", objType.GetGenericTypeDefinition().FullName); + Assert.True(objType.GetGenericTypeDefinition().FullName is "System.Collections.Generic.ObjectEqualityComparer`1" or "System.Collections.Generic.GenericEqualityComparer`1"); Assert.Equal(obj.GetType().GetGenericArguments()[0], objType.GetGenericArguments()[0]); } diff --git a/src/mono/System.Private.CoreLib/src/System/Collections/Generic/EqualityComparer.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Collections/Generic/EqualityComparer.Mono.cs index 6d6bb681e1a9e..6ea693484a8e7 100644 --- a/src/mono/System.Private.CoreLib/src/System/Collections/Generic/EqualityComparer.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Collections/Generic/EqualityComparer.Mono.cs @@ -37,8 +37,7 @@ private static EqualityComparer CreateComparer() if (t == typeof(string)) { - // Specialize for string, as EqualityComparer.Default is on the startup path - return (EqualityComparer)(object)(new GenericEqualityComparer()); + return (EqualityComparer)(object)new StringEqualityComparer(); } if (typeof(IEquatable).IsAssignableFrom(t)) diff --git a/src/tests/JIT/opt/Devirtualization/Comparer_get_Default.cs b/src/tests/JIT/opt/Devirtualization/Comparer_get_Default.cs index 5544af8529d6f..9e49c4ac69efc 100644 --- a/src/tests/JIT/opt/Devirtualization/Comparer_get_Default.cs +++ b/src/tests/JIT/opt/Devirtualization/Comparer_get_Default.cs @@ -387,7 +387,7 @@ private static void GetTypeTests() AssertEquals("System.Collections.Generic.GenericEqualityComparer`1[System.Byte]", EqualityComparer.Default.GetType().ToString()); AssertEquals("System.Collections.Generic.GenericEqualityComparer`1[System.Int32]", EqualityComparer.Default.GetType().ToString()); - AssertEquals("System.Collections.Generic.GenericEqualityComparer`1[System.String]", EqualityComparer.Default.GetType().ToString()); + AssertEquals("System.Collections.Generic.StringEqualityComparer", EqualityComparer.Default.GetType().ToString()); AssertEquals("System.Collections.Generic.GenericEqualityComparer`1[System.Guid]", EqualityComparer.Default.GetType().ToString()); AssertEquals("System.Collections.Generic.EnumEqualityComparer`1[System.Runtime.CompilerServices.MethodImplOptions]", EqualityComparer.Default.GetType().ToString()); AssertEquals("System.Collections.Generic.EnumEqualityComparer`1[CharEnum]", EqualityComparer.Default.GetType().ToString());