Tengo un resultado confuso con genéricos y restricciones que estoy tratando de entender. Dejando de lado si algo de esto es una buena idea o no, este código:
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"); } }
Produce esta salida:
Result was 0 24 extra bytes allocated
Dentro de Iterate
, el bucle foreach está generando código para encuadrar el enumerador y verificar si implementa IDisposable
. El boxeo está provocando la asignación.
Si cambio la restricción de tipo para que Iterate
esté where T : struct, IValueEnumerator<int, MyEnumerable>
el compilador parece reconocer que el iterador no puede implementar IDisposable y omite el encuadre y no obtengo asignaciones:
Result was 0 0 extra bytes allocated
He probado una variedad de compiladores y todos tienen el mismo comportamiento.
¿Es esto por diseño? Creo que la cláusula de restricción original sería suficiente para que el compilador no genere la verificación IDisposable
. ¿Hay restricciones adicionales que podría agregar para que siga siendo genérico pero que el compilador no implemente la verificación IDisposable
en el foreach, o al menos no haga el boxeo?
Bien, he descubierto esto, tanto por qué estaba sucediendo como una solución.
Primero, esto solo sucede en la depuración, no en el lanzamiento. En el lanzamiento, parece que el compilador es lo suficientemente inteligente como para evitar la verificación adicional de IDisposable
.
A continuación, la restricción where T : struct, IValueEnumerator<int, MyEnumerable>
le dice al compilador que T será un tipo de valor pero no le dice que T no será IDisposasble
. En la depuración, parece que la misma función genérica debe poder ejecutarse en cualquier tipo permitido por las restricciones y algunos de esos tipos pueden implementar IDisposable
y otros no. La ruta de menor resistencia para el compilador es simplemente encuadrar el valor y verificar si es un IDisposable
.
Desafortunadamente, no hay nada como where T : not IDisposable
para asegurarle al compilador que no tendrá que deshacerse del enumerador. Sin embargo, podemos ir por el otro lado y hacer que la interfaz implemente IDisposable
. Luego, el compilador sabe que T es desechable y puede llamar a Dispose()
sin encasillarlo primero.
Es decir, no causará ningún encasillamiento ni asignaciones adicionales en la depuración si la interfaz se ve así:
public interface IValueEnumerator<T, EnumeratorType> : IDisposable where EnumeratorType : struct, IDisposable { T Current { get; } bool MoveNext(); EnumeratorType GetEnumerator(); }
La estructura de implementación tiene que agregar un método Dispose()
vacío al que llamará el foreach después de su iteración, lo cual es menos eficiente que cuando el compilador sabe que el enumerador definitivamente no implementa IDisposable
y puede omitir todo eso. Pero evita cualquier boxeo y no se hacen asignaciones.