An interface for value-type enumerators, a proposal
As I explained in a previous article, IEnumerable<T>
is an interface that enforces the requirements for the source of a foreach
statement. Any type that implements IEnumerable<T>
can be traversed using the foreach
statement.
As I explained in one other article, there’s a big advantage in performance if the enumerator is a value-type. All the collections provided by the .NET framework define value-type enumerators.
The disadvantage of using IEnumerable<T>
is exactly that it requires GetEnumerator()
to return IEnumerator<T>
which is an interface, a reference-type. There are ways to workaround this issue by providing overloads to GetEnumerator()
but these only work if the collections itself is not cast to IEnumerable<T>
.
LINQ casts all collections to IEnumerable<T>
so it uses runtime optimizations to improve performance but these make the code a lot more complex and hard to maintain.
IValueEnumerable<T, TEnumerator>
My proposal is to adopt an interface that not only requires GetEnumerator()
to return a value-type but that is also backwards compatible. Meaning that, collections that implement it, must still be handled by existing libraries that require the use of IEnumerable<T>
, e.g. LINQ.
1
2
3
4
5
6
public interface IValueEnumerable<out T, out TEnumerator>
: IEnumerable<T>
where TEnumerator : struct, IEnumerator<T>
{
new TEnumerator GetEnumerator();
}
IValueEnumerable<T, TEnumerator>
adds an overload for GetEnumerator()
that returns TEnumerator
, a generics attribute that has a struct
constraint, making it a value-type. The interface derives from IEnumerable<T>
making all collections that implement it to be backwards compatible. This also requires TEnumerator
to implement IEnumerator<T>
.
As an example, here’s a simple collection that implement IValueEnumerable<T, TEnumerator>
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class MyCollection
: IValueEnumerable<int, MyCollection.Enumerator>
{
readonly int[] source;
public MyCollection(int[] source)
=> this.source = source;
public Enumerator GetEnumerator()
=> new Enumerator(this);
IEnumerator<int> IEnumerable<int>.GetEnumerator()
=> GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public struct Enumerator : IEnumerator<int>
{
readonly int[] source;
int index;
public Enumerator(MyCollection enumerable)
{
source = enumerable.source;
index = -1;
}
public int Current
=> source[index];
object IEnumerator.Current
=> Current;
public bool MoveNext()
=> ++index < source.Length;
public void Reset()
=> index = -1;
public void Dispose() {}
}
}
In this case, changing the Enumerator
from a struct
to a class
would result in a compilation error.
The interface can also be used in the methods that process the contents of a collection. For example, we can implement a Sum()
method that takes a parameter of type IValueEnumerable<T, TEnumerator>
:
1
2
3
4
5
6
7
8
9
public static T Sum<T, TEnumerator>(this IValueEnumerable<T, TEnumerator> source)
where T: IAdditiveIdentity<T, T>, IAdditionOperators<T, T, T>
where TEnumerator : struct, IEnumerator<T>
{
var sum = T.AdditiveIdentity;
foreach(var item in source)
sum += item;
return sum;
}
NOTE: Check my other article “Generic math in .NET” to understand de use of the
IAdditiveIdentity<T, T>
andIAdditionOperators<T, T, T>
interfaces.
You can see in SharpLab that the compiled code is equivalent to the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static T Sum<T, TEnumerator>(IValueEnumerable<T, TEnumerator> source)
where T : IAdditiveIdentity<T, T>, IAdditionOperators<T, T, T>
where TEnumerator : struct, IEnumerator<T>
{
T additiveIdentity = T.AdditiveIdentity;
TEnumerator enumerator = source.GetEnumerator();
try
{
while (enumerator.MoveNext())
{
T current = enumerator.Current;
additiveIdentity += current;
}
return additiveIdentity;
}
finally
{
enumerator.Dispose();
}
}
Notice that the enumerator is of type TEnumerator
which has a constraint to be a value-type. If the source parameter had been of type IEnumerator<T>
, the enumerator would have been of type IEnumerator<T>
which is a reference-type.
NOTE: For benchmarks please check my other article “Performance of value-type vs reference-type enumerators in C#”.
NetFabric.Hyperlinq.Abstractions
An interface for enumeration becomes most useful when the developers of both the collections and the processing libraries use the same interface. This makes them interoperable.
While there is not an alternative in the .NET framework but you’d like to start using a standard version, I suggest the use of the package NetFabric.Hyperlinq.Abstractions.
This package is extensively used by the packages NetFabric.DoublyLinkedList and NetFabric.Hyperlinq.