Why is this an issue?
When testing if a collection contains a specific item by simple equality, both ICollection.Contains(T item) and
IEnumerable.Any(x ⇒ x == item) can be used. However, Any searches the data structure in a linear manner using a foreach
loop, whereas Contains is considerably faster in some collection types, because of the underlying implementation. More specifically:
HashSet<T>is a hashtable, and therefore has an O(1) lookupSortedSet<T>is a red-black tree, and therefore has a O(logN) lookupList<T>is a linear search, and therefore has an O(N) lookup, but the EqualityComparer is optimized for theTtype, which is not the case forAny
For small collections, the performance difference may be negligible, but for large collections, it can be noticeable.
What is the potential impact?
We measured a significant improvement both in execution time and memory allocation. For more details see the Benchmarks section from
the More info tab.
Exceptions
Since LINQ to
Entities relies a lot on System.Linq for query conversion,
this rule won’t raise when used within LINQ to Entities syntaxes.
How to fix it
Contains is a method defined on the ICollection<T> interface and takes a T item argument.
Any is an extension method defined on the IEnumerable<T> interface and takes a predicate argument. Therefore, calls
with simple equality checks like Any(x ⇒ x == item) can be replaced by Contains(item).
This applies to the following collection types:
Code examples
Noncompliant code example
bool ValueExists(HashSet<int> data) =>
data.Any(x => x == 42);
bool ValueExists(List<int> data) =>
data.Any(x => x == 42);
Compliant solution
bool ValueExists(HashSet<int> data) =>
data.Contains(42);
bool ValueExists(List<int> data) =>
data.Contains(42);
Resources
Documentation
Articles & blog posts
Benchmarks
| Method | Runtime | Mean | Standard Deviation | Allocated |
|---|---|---|---|---|
|
HashSet_Any |
.NET 7.0 |
35,388.333 us |
620.1863 us |
40132 B |
|
HashSet_Contains |
.NET 7.0 |
3.799 us |
0.1489 us |
- |
|
List_Any |
.NET 7.0 |
32,851.509 us |
667.1658 us |
40130 B |
|
List_Contains |
.NET 7.0 |
375.132 us |
8.0764 us |
- |
|
HashSet_Any |
.NET Framework 4.6.2 |
28,979.763 us |
678.0093 us |
40448 B |
|
HashSet_Contains |
.NET Framework 4.6.2 |
5.987 us |
0.1090 us |
- |
|
List_Any |
.NET Framework 4.6.2 |
25,830.221 us |
487.2470 us |
40448 B |
|
List_Contains |
.NET Framework 4.6.2 |
5,935.812 us |
57.7569 us |
- |
Glossary
The results were generated by running the following snippet with BenchmarkDotNet:
[Params(10_000)]
public int SampleSize;
[Params(1_000)]
public int Iterations;
private static HashSet<int> hashSet;
private static List<int> list;
[GlobalSetup]
public void Setup()
{
hashSet = new HashSet<int>(Enumerable.Range(0, SampleSize));
list = Enumerable.Range(0, SampleSize).ToList();
}
[Benchmark]
public void HashSet_Any() =>
CheckAny(hashSet, SampleSize / 2);
[Benchmark]
public void HashSet_Contains() =>
CheckContains(hashSet, SampleSize / 2);
[Benchmark]
public void List_Any() =>
CheckAny(list, SampleSize / 2);
[Benchmark]
public void List_Contains() =>
CheckContains(list, SampleSize / 2);
void CheckAny(IEnumerable<int> values, int target)
{
for (int i = 0; i < Iterations; i++)
{
_ = values.Any(x => x == target); // Enumerable.Any
}
}
void CheckContains(ICollection<int> values, int target)
{
for (int i = 0; i < Iterations; i++)
{
_ = values.Contains(target); // ICollection<T>.Contains
}
}
Hardware configuration:
BenchmarkDotNet=v0.13.5, OS=Windows 10 (10.0.19045.2846/22H2/2022Update)
11th Gen Intel Core i7-11850H 2.50GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK=7.0.203
[Host] : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2
.NET 7.0 : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2
.NET Framework 4.6.2 : .NET Framework 4.8.1 (4.8.9139.0), X64 RyuJIT VectorSize=256