Skip to content

Commit 878c0fa

Browse files
committed
Added EnumerableComparer overloads that avoid boxing for ImmutableAray. (ArraySegment, the other common collection struct, has implicit conversions from arrays, which mess with nullability constraints.)
1 parent 8f00c7f commit 878c0fa

File tree

1 file changed

+76
-36
lines changed

1 file changed

+76
-36
lines changed

DomainModeling/Comparisons/EnumerableComparer.cs

Lines changed: 76 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System.Collections;
2+
using System.Collections.Immutable;
23
using System.Diagnostics.CodeAnalysis;
4+
using System.Runtime.CompilerServices;
35

46
namespace Architect.DomainModeling.Comparisons;
57

@@ -25,6 +27,20 @@ public static int GetEnumerableHashCode([AllowNull] IEnumerable enumerable)
2527
return -1;
2628
}
2729

30+
/// <summary>
31+
/// <para>
32+
/// Returns a hash code over some of the content of the given <see cref="ImmutableArray{T}"/>.
33+
/// </para>
34+
/// </summary>
35+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
36+
public static int GetEnumerableHashCode<TElement>([AllowNull] ImmutableArray<TElement>? enumerable)
37+
{
38+
if (enumerable is not {} value) return 0;
39+
var span = value.AsSpan();
40+
if (span.Length == 0) return 1;
41+
return HashCode.Combine(span.Length, span[0], span[^1]);
42+
}
43+
2844
/// <summary>
2945
/// <para>
3046
/// Returns a hash code over some of the content of the given <see cref="IEnumerable{T}"/>.
@@ -66,6 +82,59 @@ public static int GetEnumerableHashCode<TElement>([AllowNull] IEnumerable<TEleme
6682
return GetEnumerableHashCode((IEnumerable?)enumerable);
6783
}
6884

85+
/// <summary>
86+
/// <para>
87+
/// Compares the given <see cref="IEnumerable"/> objects for equality by comparing their elements.
88+
/// </para>
89+
/// <para>
90+
/// This method performs equality checks on the <see cref="IEnumerable"/>'s elements.
91+
/// It is not recursive. To support nested collections, use custom collections that override their equality checks accordingly.
92+
/// </para>
93+
/// <para>
94+
/// <strong>This non-generic overload should be avoided if possible.</strong>
95+
/// It lacks the ability to special-case generic types, which may lead to unexpected results.
96+
/// For example, two <see cref="HashSet{T}"/> instances with an ignore-case comparer may consider each other equal despite having different-cased contents.
97+
/// However, the current method has no knowledge of their comparers or their order-agnosticism, and may return a different result.
98+
/// </para>
99+
/// <para>
100+
/// Unlike <see cref="EnumerableEquals{TElement}(IEnumerable{TElement}, IEnumerable{TElement})"/>, this method may cause boxing of elements that are of a value type.
101+
/// </para>
102+
/// </summary>
103+
public static bool EnumerableEquals([AllowNull] IEnumerable left, [AllowNull] IEnumerable right)
104+
{
105+
if (ReferenceEquals(left, right)) return true;
106+
if (left is null || right is null) return false; // Double nulls are already handled above
107+
108+
var rightEnumerator = right.GetEnumerator();
109+
using (rightEnumerator as IDisposable)
110+
{
111+
foreach (var leftElement in left)
112+
if (!rightEnumerator.MoveNext() || !Equals(leftElement, rightEnumerator.Current))
113+
return false;
114+
if (rightEnumerator.MoveNext()) return false;
115+
}
116+
117+
return true;
118+
}
119+
120+
/// <summary>
121+
/// <para>
122+
/// Compares the given <see cref="ImmutableArray{T}"/> objects for equality by comparing their elements.
123+
/// </para>
124+
/// <para>
125+
/// This method performs equality checks on the <see cref="ImmutableArray{T}"/>'s elements.
126+
/// It is not recursive. To support nested collections, use custom collections that override their equality checks accordingly.
127+
/// </para>
128+
/// </summary>
129+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
130+
public static bool EnumerableEquals<TElement>([AllowNull] ImmutableArray<TElement>? left, [AllowNull] ImmutableArray<TElement>? right)
131+
{
132+
if (left is not ImmutableArray<TElement> leftValue || right is not ImmutableArray<TElement> rightValue)
133+
return left is null & right is null;
134+
135+
return MemoryExtensions.SequenceEqual(leftValue.AsSpan(), rightValue.AsSpan());
136+
}
137+
69138
/// <summary>
70139
/// <para>
71140
/// Compares the given <see cref="IEnumerable{T}"/> objects for equality by comparing their elements.
@@ -89,7 +158,7 @@ public static bool EnumerableEquals<TElement>([AllowNull] IEnumerable<TElement>
89158
return MemoryExtensions.SequenceEqual(System.Runtime.InteropServices.CollectionsMarshal.AsSpan(leftList), System.Runtime.InteropServices.CollectionsMarshal.AsSpan(rightList));
90159
if (left is TElement[] leftArray && right is TElement[] rightArray)
91160
return MemoryExtensions.SequenceEqual(leftArray.AsSpan(), rightArray.AsSpan());
92-
if (left is System.Collections.Immutable.ImmutableArray<TElement> leftImmutableArray && right is System.Collections.Immutable.ImmutableArray<TElement> rightImmutableArray)
161+
if (left is ImmutableArray<TElement> leftImmutableArray && right is ImmutableArray<TElement> rightImmutableArray)
93162
return MemoryExtensions.SequenceEqual(leftImmutableArray.AsSpan(), rightImmutableArray.AsSpan());
94163

95164
// Prefer to index directly, to avoid allocation of an enumerator
@@ -149,41 +218,6 @@ static bool GenericEnumerableEquals(IEnumerable<TElement> leftEnumerable, IEnume
149218
}
150219
}
151220

152-
/// <summary>
153-
/// <para>
154-
/// Compares the given <see cref="IEnumerable"/> objects for equality by comparing their elements.
155-
/// </para>
156-
/// <para>
157-
/// This method performs equality checks on the <see cref="IEnumerable{T}"/>'s elements.
158-
/// It is not recursive. To support nested collections, use custom collections that override their equality checks accordingly.
159-
/// </para>
160-
/// <para>
161-
/// <strong>This non-generic overload should be avoided if possible.</strong>
162-
/// It lacks the ability to special-case generic types, which may lead to unexpected results.
163-
/// For example, two <see cref="HashSet{T}"/> instances with an ignore-case comparer may consider each other equal despite having different-cased contents.
164-
/// However, the current method has no knowledge of their comparers or their order-agnosticism, and may return a different result.
165-
/// </para>
166-
/// <para>
167-
/// Unlike <see cref="EnumerableEquals{TElement}"/>, this method may cause boxing of elements that are of a value type.
168-
/// </para>
169-
/// </summary>
170-
public static bool EnumerableEquals([AllowNull] IEnumerable left, [AllowNull] IEnumerable right)
171-
{
172-
if (ReferenceEquals(left, right)) return true;
173-
if (left is null || right is null) return false; // Double nulls are already handled above
174-
175-
var rightEnumerator = right.GetEnumerator();
176-
using (rightEnumerator as IDisposable)
177-
{
178-
foreach (var leftElement in left)
179-
if (!rightEnumerator.MoveNext() || !Equals(leftElement, rightEnumerator.Current))
180-
return false;
181-
if (rightEnumerator.MoveNext()) return false;
182-
}
183-
184-
return true;
185-
}
186-
187221
/// <summary>
188222
/// <para>
189223
/// Returns a hash code over some of the content of the given <see cref="Memory{T}"/> wrapped in a <see cref="Nullable{T}"/>.
@@ -192,6 +226,7 @@ public static bool EnumerableEquals([AllowNull] IEnumerable left, [AllowNull] IE
192226
/// For a corresponding equality check, use <see cref="MemoryExtensions.SequenceEqual{T}(ReadOnlySpan{T}, ReadOnlySpan{T})"/>.
193227
/// </para>
194228
/// </summary>
229+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
195230
public static int GetMemoryHashCode<TElement>(Memory<TElement>? memory)
196231
{
197232
return GetMemoryHashCode((ReadOnlyMemory<TElement>?)memory);
@@ -205,6 +240,7 @@ public static int GetMemoryHashCode<TElement>(Memory<TElement>? memory)
205240
/// For a corresponding equality check, use <see cref="MemoryExtensions.SequenceEqual{T}(ReadOnlySpan{T}, ReadOnlySpan{T})"/>.
206241
/// </para>
207242
/// </summary>
243+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
208244
public static int GetMemoryHashCode<TElement>(ReadOnlyMemory<TElement>? memory)
209245
{
210246
if (memory is null) return 0;
@@ -219,6 +255,7 @@ public static int GetMemoryHashCode<TElement>(ReadOnlyMemory<TElement>? memory)
219255
/// For a corresponding equality check, use <see cref="MemoryExtensions.SequenceEqual{T}(ReadOnlySpan{T}, ReadOnlySpan{T})"/>.
220256
/// </para>
221257
/// </summary>
258+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
222259
public static int GetMemoryHashCode<TElement>(Memory<TElement> memory)
223260
{
224261
return GetMemoryHashCode((ReadOnlyMemory<TElement>)memory);
@@ -232,6 +269,7 @@ public static int GetMemoryHashCode<TElement>(Memory<TElement> memory)
232269
/// For a corresponding equality check, use <see cref="MemoryExtensions.SequenceEqual{T}(ReadOnlySpan{T}, ReadOnlySpan{T})"/>.
233270
/// </para>
234271
/// </summary>
272+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
235273
public static int GetMemoryHashCode<TElement>(ReadOnlyMemory<TElement> memory)
236274
{
237275
return GetSpanHashCode(memory.Span);
@@ -245,6 +283,7 @@ public static int GetMemoryHashCode<TElement>(ReadOnlyMemory<TElement> memory)
245283
/// For a corresponding equality check, use <see cref="MemoryExtensions.SequenceEqual{T}(ReadOnlySpan{T}, ReadOnlySpan{T})"/>.
246284
/// </para>
247285
/// </summary>
286+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
248287
public static int GetSpanHashCode<TElement>(Span<TElement> span)
249288
{
250289
return GetSpanHashCode((ReadOnlySpan<TElement>)span);
@@ -258,6 +297,7 @@ public static int GetSpanHashCode<TElement>(Span<TElement> span)
258297
/// For a corresponding equality check, use <see cref="MemoryExtensions.SequenceEqual{T}(ReadOnlySpan{T}, ReadOnlySpan{T})"/>.
259298
/// </para>
260299
/// </summary>
300+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
261301
public static int GetSpanHashCode<TElement>(ReadOnlySpan<TElement> span)
262302
{
263303
// Note that we do not distinguish between a default span and a regular empty span

0 commit comments

Comments
 (0)