Skip to content

Commit b627e2c

Browse files
committed
Implement IAlternateEqualityComparer<ReadOnlySpan<char>, string> on EqualityComparer<string>.Default
1 parent b77fef7 commit b627e2c

File tree

16 files changed

+115
-86
lines changed

16 files changed

+115
-86
lines changed

src/coreclr/System.Private.CoreLib/src/System/Collections/Generic/ComparerHelpers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ internal static object CreateDefaultEqualityComparer(Type type)
6868

6969
if (type == typeof(string))
7070
{
71-
return new GenericEqualityComparer<string>();
71+
return new StringEqualityComparer();
7272
}
7373
else if (type.IsAssignableTo(typeof(IEquatable<>).MakeGenericType(type)))
7474
{

src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/IntrinsicSupport/EqualityComparerHelpers.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ internal static unsafe bool IsEnum(RuntimeTypeHandle t)
5151
return t.ToMethodTable()->IsEnum;
5252
}
5353

54+
internal static unsafe bool IsString(RuntimeTypeHandle t)
55+
{
56+
return t.ToMethodTable()->IsString;
57+
}
58+
5459
// this function utilizes the template type loader to generate new
5560
// EqualityComparer types on the fly
5661
internal static object GetComparer(RuntimeTypeHandle t)
@@ -59,6 +64,11 @@ internal static object GetComparer(RuntimeTypeHandle t)
5964
RuntimeTypeHandle openComparerType = default(RuntimeTypeHandle);
6065
RuntimeTypeHandle comparerTypeArgument = default(RuntimeTypeHandle);
6166

67+
if (IsString(t))
68+
{
69+
return new StringEqualityComparer();
70+
}
71+
6272
if (RuntimeAugments.IsNullable(t))
6373
{
6474
RuntimeTypeHandle nullableType = RuntimeAugments.GetNullableType(t);

src/coreclr/nativeaot/System.Private.CoreLib/src/System/Collections/Generic/EqualityComparer.NativeAot.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ private static EqualityComparer<T> Create()
2323
// This body serves as a fallback when instantiation-specific implementation is unavailable.
2424
// If that happens, the compiler ensures we generate data structures to make the fallback work
2525
// when this method is compiled.
26+
27+
if (typeof(T) == typeof(string))
28+
{
29+
return Unsafe.As<EqualityComparer<T>>(new StringEqualityComparer());
30+
}
31+
2632
if (SupportsGenericIEquatableInterfaces)
2733
{
2834
return Unsafe.As<EqualityComparer<T>>(EqualityComparerHelpers.GetComparer(typeof(T).TypeHandle));

src/coreclr/vm/corelib.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,6 +1136,7 @@ DEFINE_METHOD(UTF8BUFFERMARSHALER, CONVERT_TO_MANAGED, ConvertToManaged, NoSig)
11361136

11371137
// Classes referenced in EqualityComparer<T>.Default optimization
11381138

1139+
DEFINE_CLASS(STRING_EQUALITYCOMPARER, CollectionsGeneric, StringEqualityComparer)
11391140
DEFINE_CLASS(ENUM_EQUALITYCOMPARER, CollectionsGeneric, EnumEqualityComparer`1)
11401141
DEFINE_CLASS(NULLABLE_EQUALITYCOMPARER, CollectionsGeneric, NullableEqualityComparer`1)
11411142
DEFINE_CLASS(GENERIC_EQUALITYCOMPARER, CollectionsGeneric, GenericEqualityComparer`1)

src/coreclr/vm/jitinterface.cpp

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8848,8 +8848,7 @@ CORINFO_CLASS_HANDLE CEEInfo::getDefaultComparerClassHelper(CORINFO_CLASS_HANDLE
88488848
TypeHandle elemTypeHnd(elemType);
88498849

88508850
// Mirrors the logic in BCL's CompareHelpers.CreateDefaultComparer
8851-
// And in compile.cpp's SpecializeComparer
8852-
//
8851+
88538852
// We need to find the appropriate instantiation
88548853
Instantiation inst(&elemTypeHnd, 1);
88558854

@@ -8914,16 +8913,19 @@ CORINFO_CLASS_HANDLE CEEInfo::getDefaultEqualityComparerClassHelper(CORINFO_CLAS
89148913
MODE_PREEMPTIVE;
89158914
} CONTRACTL_END;
89168915

8917-
// Mirrors the logic in BCL's CompareHelpers.CreateDefaultEqualityComparer
8918-
// And in compile.cpp's SpecializeEqualityComparer
8916+
// Mirrors the logic in BCL's CompareHelpers.CreateDefaultEqualityComparer.
89198917
TypeHandle elemTypeHnd(elemType);
89208918

8921-
// Mirrors the logic in BCL's CompareHelpers.CreateDefaultComparer
8922-
// And in compile.cpp's SpecializeComparer
8923-
//
8924-
// We need to find the appropriate instantiation
8919+
// We need to find the appropriate instantiation.
89258920
Instantiation inst(&elemTypeHnd, 1);
89268921

8922+
// string
8923+
if (elemTypeHnd.IsString())
8924+
{
8925+
TypeHandle resultTh = ((TypeHandle)CoreLibBinder::GetClass(CLASS__STRING_EQUALITYCOMPARER)).Instantiate(inst);
8926+
return CORINFO_CLASS_HANDLE(resultTh.GetMethodTable());
8927+
}
8928+
89278929
// Nullable<T>
89288930
if (Nullable::IsNullableType(elemTypeHnd))
89298931
{

src/coreclr/vm/typehandle.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@ BOOL TypeHandle::IsArray() const {
8484
return !IsTypeDesc() && AsMethodTable()->IsArray();
8585
}
8686

87+
BOOL TypeHandle::IsString() const
88+
{
89+
LIMITED_METHOD_CONTRACT;
90+
91+
return !IsTypeDesc() && AsMethodTable()->IsString();
92+
}
93+
8794
BOOL TypeHandle::IsGenericVariable() const {
8895
LIMITED_METHOD_DAC_CONTRACT;
8996

src/coreclr/vm/typehandle.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,9 @@ class TypeHandle
463463
// PTR
464464
BOOL IsPointer() const;
465465

466+
// String
467+
BOOL IsString() const;
468+
466469
// True if this type *is* a formal generic type parameter or any component of it is a formal generic type parameter
467470
BOOL ContainsGenericVariables(BOOL methodOnly=FALSE) const;
468471

src/libraries/System.Collections.Concurrent/tests/ConcurrentDictionary/ConcurrentDictionaryTests.cs

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,13 +1088,6 @@ public void ConcurrentWriteRead_NoTornValues()
10881088
}));
10891089
}
10901090

1091-
// TODO: Revise this test when EqualityComparer<string>.Default implements IAlternateEqualityComparer<ReadOnlySpan<char>, string>
1092-
[Fact]
1093-
public void GetAlternateLookup_FailsForDefaultComparer()
1094-
{
1095-
Assert.False(new ConcurrentDictionary<string, string>().TryGetAlternateLookup<ReadOnlySpan<char>>(out _));
1096-
}
1097-
10981091
[Fact]
10991092
public void GetAlternateLookup_FailsWhenIncompatible()
11001093
{
@@ -1119,17 +1112,19 @@ public void GetAlternateLookup_FailsWhenIncompatible()
11191112
[InlineData(3)]
11201113
[InlineData(4)]
11211114
[InlineData(5)]
1115+
[InlineData(6)]
11221116
public void GetAlternateLookup_OperationsMatchUnderlyingDictionary(int mode)
11231117
{
11241118
// Test with a variety of comparers to ensure that the alternate lookup is consistent with the underlying dictionary
11251119
ConcurrentDictionary<string, int> dictionary = new(mode switch
11261120
{
1127-
0 => StringComparer.Ordinal,
1128-
1 => StringComparer.OrdinalIgnoreCase,
1129-
2 => StringComparer.InvariantCulture,
1130-
3 => StringComparer.InvariantCultureIgnoreCase,
1131-
4 => StringComparer.CurrentCulture,
1132-
5 => StringComparer.CurrentCultureIgnoreCase,
1121+
0 => EqualityComparer<string>.Default,
1122+
1 => StringComparer.Ordinal,
1123+
2 => StringComparer.OrdinalIgnoreCase,
1124+
3 => StringComparer.InvariantCulture,
1125+
4 => StringComparer.InvariantCultureIgnoreCase,
1126+
5 => StringComparer.CurrentCulture,
1127+
6 => StringComparer.CurrentCultureIgnoreCase,
11331128
_ => throw new ArgumentOutOfRangeException(nameof(mode))
11341129
});
11351130
ConcurrentDictionary<string, int>.AlternateLookup<ReadOnlySpan<char>> lookup = dictionary.GetAlternateLookup<ReadOnlySpan<char>>();
@@ -1165,7 +1160,8 @@ public void GetAlternateLookup_OperationsMatchUnderlyingDictionary(int mode)
11651160

11661161
// Ensure that case-sensitivity of the comparer is respected
11671162
lookup["a".AsSpan()] = 42;
1168-
if (dictionary.Comparer.Equals(StringComparer.Ordinal) ||
1163+
if (dictionary.Comparer.Equals(EqualityComparer<string>.Default) ||
1164+
dictionary.Comparer.Equals(StringComparer.Ordinal) ||
11691165
dictionary.Comparer.Equals(StringComparer.InvariantCulture) ||
11701166
dictionary.Comparer.Equals(StringComparer.CurrentCulture))
11711167
{

src/libraries/System.Collections.Immutable/tests/Frozen/FrozenDictionaryAlternateLookupTests.cs

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,10 @@ public class FrozenDictionaryAlternateLookupTests
1212
[Fact]
1313
public void AlternateLookup_Empty()
1414
{
15-
FrozenDictionary<string, string>[] unsupported =
15+
FrozenDictionary<string, string>[] supported =
1616
[
17-
FrozenDictionary<string, string>.Empty,
1817
FrozenDictionary.ToFrozenDictionary<string, string>([]),
1918
FrozenDictionary.ToFrozenDictionary<string, string>([], EqualityComparer<string>.Default),
20-
];
21-
foreach (FrozenDictionary<string, string> frozen in unsupported)
22-
{
23-
Assert.Throws<InvalidOperationException>(() => frozen.GetAlternateLookup<ReadOnlySpan<char>>());
24-
Assert.False(frozen.TryGetAlternateLookup<ReadOnlySpan<char>>(out _));
25-
}
26-
27-
FrozenDictionary<string, string>[] supported =
28-
[
2919
FrozenDictionary.ToFrozenDictionary<string, string>([], StringComparer.Ordinal),
3020
FrozenDictionary.ToFrozenDictionary<string, string>([], StringComparer.OrdinalIgnoreCase),
3121
];
@@ -39,7 +29,7 @@ public void AlternateLookup_Empty()
3929
[Fact]
4030
public void UnsupportedComparer_ThrowsOrReturnsFalse()
4131
{
42-
FrozenDictionary<string, int> frozen = new Dictionary<string, int> { ["a"] = 1, ["b"] = 2 }.ToFrozenDictionary();
32+
FrozenDictionary<char, int> frozen = new Dictionary<char, int> { ['a'] = 1, ['b'] = 2 }.ToFrozenDictionary();
4333
Assert.Throws<InvalidOperationException>(() => frozen.GetAlternateLookup<ReadOnlySpan<char>>());
4434
Assert.False(frozen.TryGetAlternateLookup<ReadOnlySpan<char>>(out _));
4535
}

src/libraries/System.Collections.Immutable/tests/Frozen/FrozenSetAlternateLookupTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ public class FrozenSetAlternateLookupTests
1212
[Fact]
1313
public void AlternateLookup_Empty()
1414
{
15-
Assert.False(FrozenSet<string>.Empty.TryGetAlternateLookup<ReadOnlySpan<char>>(out _));
15+
Assert.True(FrozenSet<string>.Empty.TryGetAlternateLookup<ReadOnlySpan<char>>(out _));
1616

17-
foreach (StringComparer comparer in new[] { StringComparer.Ordinal, StringComparer.OrdinalIgnoreCase })
17+
foreach (IEqualityComparer<string> comparer in new IEqualityComparer<string>[] { null, EqualityComparer<string>.Default, StringComparer.Ordinal, StringComparer.OrdinalIgnoreCase })
1818
{
1919
FrozenSet<string>.AlternateLookup<ReadOnlySpan<char>> lookup = FrozenSet.ToFrozenSet([], comparer).GetAlternateLookup<ReadOnlySpan<char>>();
2020
Assert.False(lookup.Contains("anything".AsSpan()));
@@ -24,7 +24,7 @@ public void AlternateLookup_Empty()
2424
[Fact]
2525
public void UnsupportedComparer()
2626
{
27-
FrozenSet<string> frozen = FrozenSet.ToFrozenSet(["a", "b"]);
27+
FrozenSet<char> frozen = FrozenSet.ToFrozenSet(['a', 'b']);
2828
Assert.Throws<InvalidOperationException>(() => frozen.GetAlternateLookup<ReadOnlySpan<char>>());
2929
Assert.False(frozen.TryGetAlternateLookup<ReadOnlySpan<char>>(out _));
3030
}

0 commit comments

Comments
 (0)