Skip to content

Commit 824116a

Browse files
authored
Clean up HasDuplicates extension method (#78808)
1 parent 6badbad commit 824116a

File tree

9 files changed

+297
-73
lines changed

9 files changed

+297
-73
lines changed

src/Compilers/CSharp/Portable/Symbols/ConstraintsHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -804,7 +804,7 @@ private static bool HasDuplicateInterfaces(NamedTypeSymbol type, ConsList<TypeSy
804804
// some implemented interfaces are related
805805
// will have to instantiate interfaces and check
806806
hasRelatedInterfaces:
807-
return type.InterfacesNoUseSiteDiagnostics(basesBeingResolved).HasDuplicates(Symbols.SymbolEqualityComparer.IgnoringDynamicTupleNamesAndNullability);
807+
return type.InterfacesNoUseSiteDiagnostics(basesBeingResolved).HasDuplicates<NamedTypeSymbol>(SymbolEqualityComparer.IgnoringDynamicTupleNamesAndNullability);
808808
}
809809

810810
public static bool CheckConstraints(

src/Compilers/Core/CodeAnalysisTest/InternalUtilities/EnumerableExtensionsTests.cs renamed to src/Compilers/Core/CodeAnalysisTest/Collections/Extensions/EnumerableExtensionsTests.cs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ namespace Microsoft.CodeAnalysis.UnitTests;
1717
public class EnumerableExtensionsTests
1818
{
1919
private static IEnumerable<T> MakeEnumerable<T>(params T[] values)
20-
=> values;
20+
{
21+
foreach (var value in values)
22+
{
23+
yield return value;
24+
}
25+
}
2126

2227
[Fact]
2328
public void SequenceEqual()
@@ -154,5 +159,46 @@ public void TestSequenceEqualWithFunction()
154159
Assert.False(seq.SequenceEqual(new int[] { 1, 2, 6 }, equality));
155160
}
156161

162+
public sealed class Comparer<T>(Func<T, T, bool> equals, Func<T, int> hashCode) : IEqualityComparer<T>
163+
{
164+
private readonly Func<T, T, bool> _equals = equals;
165+
private readonly Func<T, int> _hashCode = hashCode;
166+
167+
public bool Equals(T x, T y) => _equals(x, y);
168+
public int GetHashCode(T obj) => _hashCode(obj);
169+
}
170+
171+
[Fact]
172+
public void HasDuplicates()
173+
{
174+
var comparer = new Comparer<int>((x, y) => x % 10 == y % 10, x => (x % 10).GetHashCode());
175+
176+
Assert.False(MakeEnumerable<int>().HasDuplicates());
177+
Assert.False(MakeEnumerable<int>().HasDuplicates(comparer));
178+
Assert.False(MakeEnumerable<int>().HasDuplicates(i => i + 1));
179+
180+
Assert.False(MakeEnumerable(1).HasDuplicates());
181+
Assert.False(MakeEnumerable(1).HasDuplicates(comparer));
182+
Assert.False(MakeEnumerable(1).HasDuplicates(i => i + 1));
183+
184+
Assert.False(MakeEnumerable(1, 2).HasDuplicates());
185+
Assert.False(MakeEnumerable(1, 2).HasDuplicates(comparer));
186+
Assert.False(MakeEnumerable(1, 2).HasDuplicates(i => i + 1));
187+
188+
Assert.True(MakeEnumerable(1, 1).HasDuplicates());
189+
Assert.True(MakeEnumerable(11, 1).HasDuplicates(comparer));
190+
Assert.True(MakeEnumerable(1, 3).HasDuplicates(i => i % 2));
191+
Assert.True(MakeEnumerable(11.0, 1.2).HasDuplicates(i => (int)i, comparer));
192+
193+
Assert.False(MakeEnumerable(2, 0, 1, 3).HasDuplicates());
194+
Assert.False(MakeEnumerable(2, 0, 1, 13).HasDuplicates(comparer));
195+
Assert.False(MakeEnumerable(2, 0, 1, 53).HasDuplicates(i => i % 10));
196+
Assert.False(MakeEnumerable(2.3, 0.1, 1.3, 53.4).HasDuplicates(i => (int)i, comparer));
197+
198+
Assert.True(MakeEnumerable(2, 0, 1, 2).HasDuplicates());
199+
Assert.True(MakeEnumerable(2, 0, 1, 12).HasDuplicates(comparer));
200+
Assert.True(MakeEnumerable(2, 0, 1, 52).HasDuplicates(i => i % 10));
201+
Assert.True(MakeEnumerable(2.3, 0.1, 1.3, 52.4).HasDuplicates(i => (int)i, comparer));
202+
}
157203
}
158204

src/Compilers/Core/CodeAnalysisTest/Collections/ImmutableArrayExtensionsTests.cs renamed to src/Compilers/Core/CodeAnalysisTest/Collections/Extensions/ImmutableArrayExtensionsTests.cs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,12 @@
77
using System;
88
using System.Collections.Generic;
99
using System.Collections.Immutable;
10-
using System.Collections.ObjectModel;
1110
using System.Linq;
1211
using Microsoft.CodeAnalysis.PooledObjects;
1312
using Roslyn.Test.Utilities;
1413
using Xunit;
1514

16-
namespace Microsoft.CodeAnalysis.UnitTests.Collections
15+
namespace Microsoft.CodeAnalysis.UnitTests
1716
{
1817
public class ImmutableArrayExtensionsTests
1918
{
@@ -544,5 +543,47 @@ public void IsSubsetOf_Equal(int[] array, int[] other)
544543
Assert.True(array.ToImmutableArray().IsSubsetOf(other.ToImmutableArray()));
545544
Assert.True(other.ToImmutableArray().IsSubsetOf(array.ToImmutableArray()));
546545
}
546+
547+
public sealed class Comparer<T>(Func<T, T, bool> equals, Func<T, int> hashCode) : IEqualityComparer<T>
548+
{
549+
private readonly Func<T, T, bool> _equals = equals;
550+
private readonly Func<T, int> _hashCode = hashCode;
551+
552+
public bool Equals(T x, T y) => _equals(x, y);
553+
public int GetHashCode(T obj) => _hashCode(obj);
554+
}
555+
556+
[Fact]
557+
public void HasDuplicates()
558+
{
559+
var comparer = new Comparer<int>((x, y) => x % 10 == y % 10, x => (x % 10).GetHashCode());
560+
561+
Assert.False(ImmutableArray<int>.Empty.HasDuplicates());
562+
Assert.False(ImmutableArray<int>.Empty.HasDuplicates(comparer));
563+
Assert.False(ImmutableArray<int>.Empty.HasDuplicates(i => i + 1));
564+
565+
Assert.False(ImmutableArray.Create(1).HasDuplicates());
566+
Assert.False(ImmutableArray.Create(1).HasDuplicates(comparer));
567+
Assert.False(ImmutableArray.Create(1).HasDuplicates(i => i + 1));
568+
569+
Assert.False(ImmutableArray.Create(1, 2).HasDuplicates());
570+
Assert.False(ImmutableArray.Create(1, 2).HasDuplicates(comparer));
571+
Assert.False(ImmutableArray.Create(1, 2).HasDuplicates(i => i + 1));
572+
573+
Assert.True(ImmutableArray.Create(1, 1).HasDuplicates());
574+
Assert.True(ImmutableArray.Create(11, 1).HasDuplicates(comparer));
575+
Assert.True(ImmutableArray.Create(1, 3).HasDuplicates(i => i % 2));
576+
Assert.True(ImmutableArray.Create(11.0, 1.2).HasDuplicates(i => (int)i, comparer));
577+
578+
Assert.False(ImmutableArray.Create(2, 0, 1, 3).HasDuplicates());
579+
Assert.False(ImmutableArray.Create(2, 0, 1, 13).HasDuplicates(comparer));
580+
Assert.False(ImmutableArray.Create(2, 0, 1, 53).HasDuplicates(i => i % 10));
581+
Assert.False(ImmutableArray.Create(2.3, 0.1, 1.3, 53.4).HasDuplicates(i => (int)i, comparer));
582+
583+
Assert.True(ImmutableArray.Create(2, 0, 1, 2).HasDuplicates());
584+
Assert.True(ImmutableArray.Create(2, 0, 1, 12).HasDuplicates(comparer));
585+
Assert.True(ImmutableArray.Create(2, 0, 1, 52).HasDuplicates(i => i % 10));
586+
Assert.True(ImmutableArray.Create(2.3, 0.1, 1.3, 52.4).HasDuplicates(i => (int)i, comparer));
587+
}
547588
}
548589
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
#nullable disable
6+
7+
using System;
8+
using System.Collections.Generic;
9+
using Roslyn.Utilities;
10+
using Xunit;
11+
12+
namespace Microsoft.CodeAnalysis.UnitTests;
13+
14+
public class ListExtensionsTests
15+
{
16+
public sealed class Comparer<T>(Func<T, T, bool> equals, Func<T, int> hashCode) : IEqualityComparer<T>
17+
{
18+
private readonly Func<T, T, bool> _equals = equals;
19+
private readonly Func<T, int> _hashCode = hashCode;
20+
21+
public bool Equals(T x, T y) => _equals(x, y);
22+
public int GetHashCode(T obj) => _hashCode(obj);
23+
}
24+
25+
[Fact]
26+
public void HasDuplicates()
27+
{
28+
var comparer = new Comparer<int>((x, y) => x % 10 == y % 10, x => (x % 10).GetHashCode());
29+
30+
Assert.False(new int[0].HasDuplicates());
31+
Assert.False(new int[0].HasDuplicates(comparer));
32+
Assert.False(new int[0].HasDuplicates(i => i + 1));
33+
34+
Assert.False(new[] { 1 }.HasDuplicates());
35+
Assert.False(new[] { 1 }.HasDuplicates(comparer));
36+
Assert.False(new[] { 1 }.HasDuplicates(i => i + 1));
37+
38+
Assert.False(new[] { 1, 2 }.HasDuplicates());
39+
Assert.False(new[] { 1, 2 }.HasDuplicates(comparer));
40+
Assert.False(new[] { 1, 2 }.HasDuplicates(i => i + 1));
41+
42+
Assert.True(new[] { 1, 1 }.HasDuplicates());
43+
Assert.True(new[] { 11, 1 }.HasDuplicates(comparer));
44+
Assert.True(new[] { 1, 3 }.HasDuplicates(i => i % 2));
45+
Assert.True(new[] { 11.0, 1.2 }.HasDuplicates(i => (int)i, comparer));
46+
47+
Assert.False(new[] { 2, 0, 1, 3 }.HasDuplicates());
48+
Assert.False(new[] { 2, 0, 1, 13 }.HasDuplicates(comparer));
49+
Assert.False(new[] { 2, 0, 1, 53 }.HasDuplicates(i => i % 10));
50+
Assert.False(new[] { 2.3, 0.1, 1.3, 53.4 }.HasDuplicates(i => (int)i, comparer));
51+
52+
Assert.True(new[] { 2, 0, 1, 2 }.HasDuplicates());
53+
Assert.True(new[] { 2, 0, 1, 12 }.HasDuplicates(comparer));
54+
Assert.True(new[] { 2, 0, 1, 52 }.HasDuplicates(i => i % 10));
55+
Assert.True(new[] { 2.3, 0.1, 1.3, 52.4 }.HasDuplicates(i => (int)i, comparer));
56+
}
57+
}
58+

src/Dependencies/Collections/Extensions/IEnumerableExtensions.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,72 @@ public static bool IsEmpty<T>(this List<T> source)
252252
return source.Count == 0;
253253
}
254254

255+
public static bool HasDuplicates<T>(this IEnumerable<T> source)
256+
=> source.HasDuplicates(EqualityComparer<T>.Default);
257+
258+
public static bool HasDuplicates<T>(this IEnumerable<T> source, IEqualityComparer<T> comparer)
259+
=> source.HasDuplicates(static x => x, comparer);
260+
261+
public static bool HasDuplicates<TItem, TValue>(this IEnumerable<TItem> source, Func<TItem, TValue> selector)
262+
=> source.HasDuplicates(selector, EqualityComparer<TValue>.Default);
263+
264+
/// <summary>
265+
/// Determines whether duplicates exist using given equality comparer.
266+
/// </summary>
267+
/// <param name="source">Array to search for duplicates</param>
268+
/// <returns>Whether duplicates were found</returns>
269+
/// <remarks>
270+
/// API proposal: https://github.com/dotnet/runtime/issues/30582.
271+
/// <seealso cref="Microsoft.CodeAnalysis.ImmutableArrayExtensions.HasDuplicates{TItem, TValue}(ImmutableArray{TItem}, Func{TItem, TValue}, IEqualityComparer{TValue})"/>
272+
/// </remarks>
273+
public static bool HasDuplicates<TItem, TValue>(this IEnumerable<TItem> source, Func<TItem, TValue> selector, IEqualityComparer<TValue> comparer)
274+
{
275+
if (source is IReadOnlyList<TItem> list)
276+
{
277+
return list.HasDuplicates(selector, comparer);
278+
}
279+
280+
TItem firstItem = default!;
281+
HashSet<TValue>? set = null;
282+
var isFirstItem = true;
283+
var result = false;
284+
285+
foreach (var item in source)
286+
{
287+
if (isFirstItem)
288+
{
289+
firstItem = item;
290+
isFirstItem = false;
291+
continue;
292+
}
293+
294+
var value = selector(item);
295+
296+
if (set == null)
297+
{
298+
var firstValue = selector(firstItem);
299+
300+
if (comparer.Equals(value, firstValue))
301+
{
302+
result = true;
303+
break;
304+
}
305+
306+
set = comparer == EqualityComparer<TValue>.Default ? PooledHashSet<TValue>.GetInstance() : new HashSet<TValue>(comparer);
307+
set.Add(firstValue);
308+
set.Add(value);
309+
}
310+
else if (!set.Add(value))
311+
{
312+
result = true;
313+
break;
314+
}
315+
}
316+
317+
(set as PooledHashSet<TValue>)?.Free();
318+
return result;
319+
}
320+
255321
private static readonly Func<object, bool> s_notNullTest = x => x != null;
256322

257323
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using Microsoft.CodeAnalysis.PooledObjects;
8+
9+
namespace Microsoft.CodeAnalysis;
10+
11+
internal static class IListExtensions
12+
{
13+
public static bool HasDuplicates<T>(this IReadOnlyList<T> list)
14+
=> list.HasDuplicates(EqualityComparer<T>.Default);
15+
16+
public static bool HasDuplicates<T>(this IReadOnlyList<T> list, IEqualityComparer<T> comparer)
17+
=> list.HasDuplicates(static x => x, comparer);
18+
19+
public static bool HasDuplicates<TItem, TValue>(this IReadOnlyList<TItem> list, Func<TItem, TValue> selector)
20+
=> list.HasDuplicates(selector, EqualityComparer<TValue>.Default);
21+
22+
/// <summary>
23+
/// Determines whether duplicates exist using given equality comparer.
24+
/// </summary>
25+
/// <param name="list">Array to search for duplicates</param>
26+
/// <returns>Whether duplicates were found</returns>
27+
/// <remarks>
28+
/// API proposal: https://github.com/dotnet/runtime/issues/30582.
29+
/// <seealso cref="ImmutableArrayExtensions.HasDuplicates{TItem, TValue}(System.Collections.Immutable.ImmutableArray{TItem}, Func{TItem, TValue}, IEqualityComparer{TValue})"/>
30+
/// <seealso cref="Roslyn.Utilities.EnumerableExtensions.HasDuplicates{TItem, TValue}(IEnumerable{TItem}, Func{TItem, TValue}, IEqualityComparer{TValue})"/>
31+
/// </remarks>
32+
internal static bool HasDuplicates<TItem, TValue>(this IReadOnlyList<TItem> list, Func<TItem, TValue> selector, IEqualityComparer<TValue> comparer)
33+
{
34+
switch (list.Count)
35+
{
36+
case 0:
37+
case 1:
38+
return false;
39+
40+
case 2:
41+
return comparer.Equals(selector(list[0]), selector(list[1]));
42+
43+
default:
44+
var set = comparer == EqualityComparer<TValue>.Default ? PooledHashSet<TValue>.GetInstance() : new HashSet<TValue>(comparer);
45+
var result = false;
46+
47+
// index to avoid allocating enumerator
48+
for (int i = 0, n = list.Count; i < n; i++)
49+
{
50+
if (!set.Add(selector(list[i])))
51+
{
52+
result = true;
53+
break;
54+
}
55+
}
56+
57+
(set as PooledHashSet<TValue>)?.Free();
58+
return result;
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)