2010-07-02 7 views
43

Todos sabemos mutable structs are evil en general. También estoy bastante seguro de que, dado que IEnumerable<T>.GetEnumerator() devuelve el tipo IEnumerator<T>, las estructuras se encajonan inmediatamente en un tipo de referencia, que cuesta más que si fueran simplemente tipos de referencia para empezar.¿Por qué las colecciones de BCL usan enumeradores de estructuras, no clases?

Entonces, ¿por qué, en las colecciones genéricas de BCL, están todas las estructuras mutables de los enumeradores? 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 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 ello.

+0

páginas relacionadas para los demás corren a través de esto: http://stackoverflow.com/questions/384511/enumerator-implementation-use-struct-or-class http://www.eggheadcafe.com/software/ aspnet/31702392/c-compiler-challenge - s.aspx –

Respuesta

62

De hecho, es por motivos de rendimiento. El equipo de BCL hizo un lote de investigación sobre este punto antes de decidirse por lo que con razón califica como 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 compilador 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) 

el primero que hacemos es comprobar para ver 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 ints, pero se escribió antes de que se inventaran los tipos genéricos, 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 el Enumerador.

Como he dicho, el equipo BCL hicieron un gran trabajo de investigación sobre este y descubrieron que la gran mayoría de las veces, la pena de asignar y desasignar el empadronador era lo suficientemente grande que valía la pena por lo que es un tipo de valor , aunque hacerlo puede causar algunos errores locos.

Por ejemplo, considere esto:

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

Se podría esperar que con toda razón el intento de mutar h a fallar, y de hecho lo hace. El compilador 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 tiene:

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

lo que sucede aquí? Es razonable esperar que el compilador haga lo que hace si h fuera un campo de solo lectura: make a copy, and mutate the copy para garantizar que el método no elimine las cosas en el valor que debe eliminarse.

Sin embargo, esto entra en conflicto con nuestra intuición acerca de lo que debe suceder aquí:

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

Esperamos que haciendo un MoveNext dentro de un bloque using se mover el empadronador a la siguiente, independientemente de si se trata de una estructura o un tipo de ref.

Desafortunadamente, el compilador 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 valor tecleado mutado a través de un método es un local de lo normal, entonces se muta normalmente

  • pero si se trata de un local de izado (porque es un cerrado sobre la variable de una función anónima o en un bloque iterador) entonces el local es realmente generado como un campo de solo lectura, y el engranaje que asegura que las mutaciones suceden en una copia toma el control.

Desafortunadamente, la especificación proporciona poca orientación sobre este asunto. Claramente, algo se rompe porque lo estamos haciendo de manera inconsistente, pero lo que la derecha hacer no está nada clara.

+1

+1 Esto significa que hay penalizaciones (mínimas) en el rendimiento por pasar un 'IEnumerable ' en oposición a la colección genérica original, en una prueba rápida en modo de publicación que enumera una 'Lista ' de diez millones de entradas, ambas directamente y cuando se convierte en un 'IEnumerable ', veo una diferencia de tiempo consistente de 2: 1 (en este caso, ~ 100ms vs ~ 50ms). –

+0

Buena respuesta, no sabía sobre esa optimización, pero tiene mucho sentido. Encuentro algo irónico que vinculé tu blog para respaldar mi afirmación de que las estructuras mutables son malvadas, y respondes a mi pregunta :) – Eloff

+0

Por cierto, es un caso de esquina desagradable, otro problema más con las estructuras mutables. – Eloff

5

Los métodos de estructura están subrayados cuando se conoce el tipo de estructura en tiempo de compilación, y el método de llamada a través de la interfaz es lento, por lo que la respuesta es: por motivos de rendimiento.

+0

Pero estas son estructuras internas, por lo que el tipo nunca se conoce en tiempo de compilación; y todo el código de usuario final accede a ellos a través de interfaces. –

+1

@Stephen: 'List .Enumerator' está documentado como público en MSDN ... –

+0

Si mira por ejemplo List . Método GetEnunmerator (http://msdn.microsoft.com/en-us/library/b0yss765 .aspx) puede ver que devuelve List :: Enumerator struct. foreach loop en C# no usa la interfaz IEnumerable directamente, es suficiente si la clase tiene el método GetEnumerator. Por lo tanto, el tipo de enumerador se conoce en tiempo de compilación. – STO

Cuestiones relacionadas