I have a confusing result with generics and constraints I'm trying to understand. Leaving aside whether any of this is a good idea or not, this code:
using System;
using System.Collections.Generic;
public interface IValueEnumerator<T, EnumeratorType> where EnumeratorType : struct
{
T Current { get; }
bool MoveNext();
EnumeratorType GetEnumerator();
}
public struct MyEnumerable : IValueEnumerator<int, MyEnumerable>
{
private int index;
private int count;
public MyEnumerable(int count)
{
this.index = -1;
this.count = count;
}
public bool MoveNext() => ++index < count;
public MyEnumerable GetEnumerator() => this;
public int Current => index;
}
public class Program
{
public static int Iterate<T>(T enumerable) where T : struct, IValueEnumerator<int, T>
{
int z = 0;
foreach (int y in enumerable)
z += y;
return z;
}
public static void Main()
{
var prevMem = System.GC.GetAllocatedBytesForCurrentThread();
MyEnumerable z = new MyEnumerable();
int result = Iterate(z);
long diff = GC.GetAllocatedBytesForCurrentThread() - prevMem;
Console.WriteLine($"Result was {result}");
Console.WriteLine($"{diff} extra bytes allocated");
}
}
Produces this output:
Result was 0
24 extra bytes allocated
Inside Iterate the foreach loop is generating code to box the enumerator and check if it implements IDisposable. The boxing is causing the allocation.
If I change the type constraint for Iterate to be where T : struct, IValueEnumerator<int, MyEnumerable> the compiler seems to recognize that the iterator can't implement IDisposable and it skips the boxing and I get no allocations:
Result was 0
0 extra bytes allocated
I've tried on a variety of compilers and they all have the same behavior.
Is this by design? I would think the original constraint clause would be enough for the compiler not to generate the IDisposable check. Are there additional constraints I could add so that it would still be generic but have the compiler not implement the IDisposable check in the foreach, or at the least not do the boxing?