¿Por qué las Colecciones BCL usan enumeradores de estructuras, no clases?

Todos sabemos que las estructuras mutables son malvadas en general. También estoy bastante seguro de que, debido a que IEnumerable.GetEnumerator() devuelve el tipo IEnumerator , las estructuras se agrupan inmediatamente en un tipo de referencia, que cuesta más que si fueran simplemente tipos de referencia para empezar.

Entonces, ¿por qué, en las colecciones genéricas de BCL, todos los enumeradores son estructuras mutables? Seguramente tenía que haber una buena razón. Lo único que se me ocurre es que las estructuras se pueden copiar fácilmente, preservando así el estado del enumerador en un punto arbitrario. Pero agregar un método Copy() a la interfaz de IEnumerator habría sido menos problemático, por lo que no veo esto como una justificación lógica por sí misma.

Incluso si no estoy de acuerdo con una decisión de diseño, me gustaría poder entender el razonamiento detrás de esto.

De hecho, es por motivos de rendimiento. El equipo de BCL realizó una gran cantidad de investigaciones sobre este punto antes de decidirse por lo que con razón se considera una práctica sospechosa y peligrosa: el uso de un tipo de valor variable.

Usted pregunta por qué esto no causa el boxeo. ¡Es porque el comstackdor de C # no genera código para encapsular cosas en IEnumerable o IEnumerator en un bucle foreach si puede evitarlo!

Cuando vemos

 foreach(X x in c) 

lo primero que hacemos es verificar si c tiene un método llamado GetEnumerator. Si lo hace, entonces verificamos si el tipo que devuelve tiene el método MoveNext y la propiedad actual. Si lo hace, entonces el bucle foreach se genera completamente usando llamadas directas a esos métodos y propiedades. Solo si “el patrón” no puede coincidir, volvemos a buscar las interfaces.

Esto tiene dos efectos deseables.

Primero, si la colección es, por ejemplo, una colección de enteros, pero fue escrita antes de que se inventaran los tipos generics, entonces no se aplica la penalización de boxeo del valor actual para objetar y luego desempaquetarlo en int. Si Current es una propiedad que devuelve un int, solo la usamos.

En segundo lugar, si el enumerador es un tipo de valor, no coloca el enumerador en IEnumerator.

Como dije, el equipo de BCL investigó mucho sobre esto y descubrió que la gran mayoría de las veces, la penalización de asignar y desasignar al enumerador era lo suficientemente grande como para que valiera la pena convertirla en un tipo de valor, aunque hacerlo puede causa algunos errores locos

Por ejemplo, considera esto:

 struct MyHandle : IDisposable { ... } ... using (MyHandle h = whatever) { h = somethingElse; } 

Con bastante razón esperarías que el bash de mutar h fallara, y de hecho lo hace. El comstackdor detecta que está tratando de cambiar el valor de algo que tiene una disposición pendiente, y que hacerlo podría causar que el objeto que necesita ser eliminado no se elimine.

Ahora supongamos que tienes:

 struct MyHandle : IDisposable { ... } ... using (MyHandle h = whatever) { h.Mutate(); } 

¿Qué pasa aquí? Es razonable esperar que el comstackdor haga lo que hace si h fuera un campo de solo lectura: haga una copia y mute la copia para asegurarse de que el método no descarta nada en el valor que debe eliminarse.

Sin embargo, eso entra en conflicto con nuestra intuición sobre lo que debería suceder aquí:

 using (Enumerator enumtor = whatever) { ... enumtor.MoveNext(); ... } 

Esperamos que al hacer un MoveNext dentro de un bloque de uso moverá el enumerador al siguiente, independientemente de si es una estructura o un tipo de ref.

Desafortunadamente, el comstackdor de C # hoy tiene un error. Si se encuentra en esta situación, elegimos qué estrategia seguir de forma inconsistente. El comportamiento de hoy es:

  • si la variable de tipo de valor que está siendo mutada a través de un método es un local normal, entonces está mutado normalmente

  • pero si se trata de un local izado (porque es una variable cerrada de una función anónima o en un bloque de iterador), entonces el local se genera realmente como un campo de solo lectura, y el engranaje que garantiza que las mutaciones sucedan en una copia encima.

Lamentablemente, la especificación proporciona poca orientación sobre este asunto. Claramente, algo se rompe porque lo estamos haciendo de manera inconsistente, pero no está claro qué es lo correcto .

Los métodos Struct están en línea cuando se conoce el tipo de estructura en tiempo de comstackción, y el método de llamada a través de la interfaz es lento, por lo que la respuesta es: debido a la razón del rendimiento.