• Jobs
  • About Us
  • professionals
    • Home
    • Jobs
    • Courses and challenges
  • business
    • Home
    • Post vacancy
    • Our process
    • Pricing
    • Assessments
    • Payroll
    • Blog
    • Sales
    • Salary Calculator

0

108
Views
Skipping the foreach IDisposable check with a Generic value type enumerator

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?

over 3 years ago · Santiago Trujillo
1 answers
Answer question

0

Okay I've figured this out, both why it was happening and a fix.

First, this only happens in debug, not in release. In release it seems the compiler is smart enough to avoid the extra IDisposable check.

Next, the where T : struct, IValueEnumerator<int, MyEnumerable> constraint tells the compiler that T will be a value type but doesn't tell it that T won't be IDisposasble. In debug it seems the same generic function has to be able to run on any types allowed by the constraints and some of those types might implement IDisposable and some might not. The path of least resistance for the compiler is to just box the value and check if it's an IDisposable.

Unfortunately there's nothing like where T : not IDisposable to assure the compiler that it won't have to dispose of the enumerator. However we can go the other way and have the interface implement IDisposable. Then the compiler knows that T is disposable and it can call Dispose() on it without boxing it first.

That is, it won't cause any boxing or extra allocations in debug if the interface looks like:

public interface IValueEnumerator<T, EnumeratorType> : IDisposable
    where EnumeratorType : struct, IDisposable
{
    T Current { get; }
    bool MoveNext();
    EnumeratorType GetEnumerator();
}

The implementing struct has to add an empty Dispose() method which the foreach will call after its iteration, which is less efficient than when the compiler knows the enumerator definitely doesn't implement IDisposable and can skip all that. But it does avoid any boxing and there aren't any allocations made.

over 3 years ago · Santiago Trujillo Report
Answer question
Find remote jobs

Discover the new way to find a job!

Top jobs
Top job categories
Business
Post vacancy Pricing Our process Sales
Legal
Terms and conditions Privacy policy
© 2025 PeakU Inc. All Rights Reserved.

Andres GPT

Recommend me some offers
I have an error