Empresas
Empregos
  • Sobre nós
  • Soluções
    • Publicação de vagas
      Publique sua vaga e receba candidatos qualificados em 48h.
    • Avaliações de candidatos
      Mais de 500 testes técnicos e psicológicos, mais anti-fraude.
    • Headhunting
      Busca executiva personalizada do início ao fim.
    • Folha de Pagamento + EOR
      Dispersão de folha e EOR em mais de 15 países da LATAM.
  • Preços
  • Empregos

0

181
Visualizações
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 4 years ago · Santiago Trujillo
1 Respostas
Responde à pergunta

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 4 years ago · Santiago Trujillo Relatório
Responde à pergunta
Encontrar trabalhos remotos

Descubra a nova forma de encontrar um emprego!

melhores empregos
Principais categorias de trabalho
Empresas
Postar vaga Preços Comercial
Jurídico
Termos e Condições Política de privacidade
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Recomende algumas ofertas para mim
Preciso de ajuda