HLQ001: Assigment causes boxing of enumerator
July 17, 2023 ยท View on GitHub
Cause
An enumerable or async enumerable, that uses value type enumerators, is assigned to a non-public variable, field or property whose type causes the enumerator to be boxed.
Severity
Warning
Rule description
Enumerables and async enumerables can be implemented so that they use value type enumerators. The advantage is that calls to its methods are not virtual and the enumerator is allocated in the stack.
Most collections in the .NET framework are implemented this way. Here's an excerpt of the implementation of System.Collections.Generic.List<T> :
public class List<T> : IList<T>, IList, IReadOnlyList<T>
{
public Enumerator GetEnumerator()
=> new Enumerator(this);
IEnumerator<T> IEnumerable<T>.GetEnumerator()
=> new Enumerator(this);
IEnumerator IEnumerable.GetEnumerator()
=> new Enumerator(this);
public struct Enumerator : IEnumerator<T>, IEnumerator
{
public bool MoveNext()
{
...
}
public T Current => _current;
object IEnumerator.Current
{
get
{
...
}
}
}
}
Notice that the public GetEnumerator() method returns the inner type Enumerator which is implemented as a struct. The other two overloads are implemented explicitly so that they are called when the List<T> is cast to one of these interfaces.
The C# foreach is implemented so that the public GetEnumerator() can be called. For example:
var list = new List<int>();
foreach (var item in list)
Console.WriteLine(item);
The compiler interprets it as the following:
List<int> list = new List<int>();
List<int>.Enumerator enumerator = list.GetEnumerator();
try
{
while (enumerator.MoveNext())
{
int current = enumerator.Current;
Console.WriteLine(current);
}
}
finally
{
((IDisposable)enumerator).Dispose();
}
The list variable is declared as type List<int> and the enumerator variable as type List<int>.Enumerator.
Changing the type of the list variable to any of the interfaces implemented by List<T>
IReadOnlyList<int> list = new List<int>();
foreach (var item in list)
Console.WriteLine(item);
results in the GetEnumerator() overload that is called now to be one implemented explicitly, which returns an interface. foreach is then expanded accordingly with the type of the enumerator variable now being IEnumerator<int> :
IReadOnlyList<int> readOnlyList = new List<int>();
IEnumerator<int> enumerator = readOnlyList.GetEnumerator();
try
{
while (enumerator.MoveNext())
{
int current = enumerator.Current;
Console.WriteLine(current);
}
}
finally
{
if (enumerator != null)
{
enumerator.Dispose();
}
}
This causes the Enumerator instance to be copied to the heap and all calls to its MoveNext() method and Current property to be virtual, reducing performance.
The state machine generated by await foreach performs exactly the same behavior when using async enumerables with value-typed enumerators.
Benchmarking
The following benchmark compares the performance of enumerating a List<int> with enumerating the same source when cast to IEnumerable<int>.
BenchmarkDotNet v0.13.6, Windows 10 (10.0.19045.3269/22H2/2022Update)
Intel Core i7-7567U CPU 3.50GHz (Kaby Lake), 1 CPU, 4 logical and 2 physical cores
.NET SDK 8.0.100-preview.5.23303.2
[Host] : .NET 6.0.20 (6.0.2023.32017), X64 RyuJIT AVX2
.NET 6 : .NET 6.0.20 (6.0.2023.32017), X64 RyuJIT AVX2
.NET 7 : .NET 7.0.8 (7.0.823.31807), X64 RyuJIT AVX2
.NET 8 : .NET 8.0.0 (8.0.23.28008), X64 RyuJIT AVX2
| Method | Job | Runtime | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Code Size | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Enumerable | .NET 6 | .NET 6.0 | 100 | 688.79 ns | 19.412 ns | 55.383 ns | 658.77 ns | baseline | **** | 0.0191 | NA | 40 B | **** |
| List | .NET 6 | .NET 6.0 | 100 | 144.42 ns | 2.604 ns | 3.898 ns | 142.93 ns | 4.78x faster | 0.41x | - | 99 B | - | NA |
| Enumerable | .NET 7 | .NET 7.0 | 100 | 759.17 ns | 10.801 ns | 9.019 ns | 758.33 ns | baseline | 0.0191 | NA | 40 B | ||
| List | .NET 7 | .NET 7.0 | 100 | 119.67 ns | 2.774 ns | 7.824 ns | 115.64 ns | 6.30x faster | 0.45x | - | NA | - | NA |
| Enumerable | .NET 8 | .NET 8.0 | 100 | 205.70 ns | 4.133 ns | 10.521 ns | 201.16 ns | baseline | 0.0191 | NA | 40 B | ||
| List | .NET 8 | .NET 8.0 | 100 | 71.41 ns | 1.463 ns | 3.777 ns | 69.56 ns | 2.88x faster | 0.17x | - | NA | - | NA |
| Enumerable | .NET 6 | .NET 6.0 | 10000 | 67,502.42 ns | 1,642.014 ns | 4,522.582 ns | 66,228.52 ns | baseline | **** | - | NA | 40 B | **** |
| List | .NET 6 | .NET 6.0 | 10000 | 15,093.08 ns | 695.963 ns | 1,962.976 ns | 14,331.64 ns | 4.53x faster | 0.58x | - | 99 B | - | NA |
| Enumerable | .NET 7 | .NET 7.0 | 10000 | 75,459.92 ns | 1,443.395 ns | 3,725.862 ns | 73,818.92 ns | baseline | - | NA | 40 B | ||
| List | .NET 7 | .NET 7.0 | 10000 | 10,823.01 ns | 193.144 ns | 257.841 ns | 10,703.44 ns | 7.06x faster | 0.46x | - | NA | - | NA |
| Enumerable | .NET 8 | .NET 8.0 | 10000 | 17,761.84 ns | 121.790 ns | 113.922 ns | 17,733.51 ns | baseline | - | NA | 40 B | ||
| List | .NET 8 | .NET 8.0 | 10000 | 5,675.34 ns | 94.276 ns | 200.909 ns | 5,600.56 ns | 3.16x faster | 0.10x | - | NA | - | NA |
How to fix violations
Change the type of the variable, field or property so that boxing is avoided. Using var, instead of the explicit type, guarantees that the correct type is always used.
When to suppress warnings
Optionally, when foreach is not used to enumerate the collection.
Example of a violation
The following example shows the multiples cases of assignment that are detected by this rule:
class Class1
{
IList<int> privateField = new List<int>();
IAsyncEnumerable<int> privateAsyncField = new OptimizedAsyncEnumerable();
IList<int> PrivateProperty { get; set; } = new List<int>();
IAsyncEnumerable<int> PrivateAsyncProperty { get; set; }
= new OptimizedAsyncEnumerable();
void Method()
{
IList<int> localVariable = new List<int>();
IAsyncEnumerable<int> localAsyncVariable = new OptimizedAsyncEnumerable();
privateField = new List<int>();
PrivateProperty = new List<int>();
localVariable = new List<int>();
privateAsyncField = new OptimizedAsyncEnumerable();
PrivateAsyncProperty = new OptimizedAsyncEnumerable();
localAsyncVariable = new OptimizedAsyncEnumerable();
}
}
Example of how to fix
Changing the types List<int> or using var for the local variable declaration, solves the issue:
class Class1
{
List<int> privateField = new List<int>();
OptimizedAsyncEnumerable privateAsyncField = new OptimizedAsyncEnumerable();
List<int> PrivateProperty { get; set; } = new List<int>();
OptimizedAsyncEnumerable PrivateAsyncProperty { get; set; }
= new OptimizedAsyncEnumerable();
void Method()
{
var localVariable = new List<int>();
var localAsyncVariable = new OptimizedAsyncEnumerable();
privateField = new List<int>();
PrivateProperty = new List<int>();
localVariable = new List<int>();
privateAsyncField = new OptimizedAsyncEnumerable();
PrivateAsyncProperty = new OptimizedAsyncEnumerable();
localAsyncVariable = new OptimizedAsyncEnumerable();
}
}