2010-05-30 23 views
11

No mucho tiempo antes de que he descubierto, que la nueva dynamic palabra clave no funciona bien con el C# 's foreach declaración:C# 4.0 'dinámico' y sentencia foreach

using System; 

sealed class Foo { 
    public struct FooEnumerator { 
     int value; 
     public bool MoveNext() { return true; } 
     public int Current { get { return value++; } } 
    } 

    public FooEnumerator GetEnumerator() { 
     return new FooEnumerator(); 
    } 

    static void Main() { 
     foreach (int x in new Foo()) { 
      Console.WriteLine(x); 
      if (x >= 100) break; 
     } 

     foreach (int x in (dynamic)new Foo()) { // :) 
      Console.WriteLine(x); 
      if (x >= 100) break; 
     } 
    } 
} 

he esperado que la iteración en el dynamic variable debería funcionar completamente como si el tipo de variable de recopilación se conociera en tiempo de compilación. He descubierto que el segundo bucle en realidad se parece a esto cuando se compila:

foreach (object x in (IEnumerable) /* dynamic cast */ (object) new Foo()) { 
    ... 
} 

y cada acceso a la x resultados variables con el dinámicas de búsqueda/cast lo que C# ignora que he especificar la correcta x ' s escriba en la declaración de foreach - eso fue un poco sorprendente para mí ... ¡Y también, el compilador de C# ignora por completo que la colección de la variable de tipo dinámico puede implementar la interfaz IEnumerable<T>!

El comportamiento completo foreach declaración se describe en la especificación de C# 4.0 8.8.4 La instrucción foreach artículo.

Pero ... ¡Es perfectamente posible implementar el mismo comportamiento en el tiempo de ejecución! Es posible añadir una CSharpBinderFlags.ForEachCast indicador adicional, corregir el código emmited que se parece a:

foreach (int x in (IEnumerable<int>) /* dynamic cast with the CSharpBinderFlags.ForEachCast flag */ (object) new Foo()) { 
    ... 
} 

y añadir un poco de lógica adicional para CSharpConvertBinder:

  • Wrap IEnumerable colecciones y IEnumerator 's a IEnumerable<T>/IEnumerator<T>.
  • Ajustar colecciones no implementa Ienumerable<T>/IEnumerator<T> para implementar estas interfaces.

Así que hoy foreach itera sobre dynamic completamente diferente de la variable de iteración en la colección de forma estática conocida y completamente ignora la información de tipo, especificadas por el usuario. Todo lo que resulta con el comportamiento de iteración diferente (IEnumarble<T> -implementar colecciones se itera como solo IEnumerable -implementing) y más de 150x desaceleración al iterar sobre dynamic. Solución simple voluntad como resultado un rendimiento mucho mejor:

foreach (int x in (IEnumerable<int>) dynamicVariable) { 

Pero por qué debería escribir código como este?

Es muy bien para ver que a veces C# 4.0 dynamic obras completamente igual si el tipo se conoce en tiempo de compilación, pero es muy triste ver que dynamic trabajos completamente diferentes en los que puede trabaja el mismo código que estático de tipos .

Así que mi pregunta es: ¿por qué foreach sobre dynamic funciona diferente de foreach sobre cualquier otra cosa?

+0

Me importa mucho el comportamiento, eso es completamente diferente de 'foreach' estático! El problema de rendimiento es solo el resultado de un comportamiento inapropiado. ¿Por qué usar 'IEnumerable' + conversión dinámica donde se sabe estáticamente que' forech' se repetirá sobre la secuencia de enteros? – ControlFlow

Respuesta

23

En primer lugar, para explicar algunos antecedentes a los lectores que están confundidos por la pregunta: el lenguaje C# en realidad no requiere que la colección de un "foreach" implemente IEnumerable.Más bien, requiere que implemente IEnumerable, o que implemente IEnumerable<T>, o simplemente que tenga un método GetEnumerator (y que el método GetEnumerator devuelva algo con un Current y MoveNext que coincida con el patrón esperado, y así sucesivamente).

Puede parecer una característica extraña para un lenguaje estáticamente tipado como C# tener. ¿Por qué deberíamos "coincidir con el patrón"? ¿Por qué no requiere que las colecciones implementan IEnumerable?

Piensa en el mundo antes de los genéricos. Si quisieras hacer una colección de ints, tendrías que usar IEnumerable. Y, por lo tanto, cada llamada a Current encajonaría una int, y luego, por supuesto, la persona que llamaba inmediatamente la volvería a poner en int. Lo cual es lento y crea presión en el GC. Siguiendo un enfoque basado en patrones, puedes crear colecciones fuertemente tipadas en C# 1.0.

Actualmente, por supuesto, nadie implementa ese patrón; si desea una colección fuertemente tipada, implemente IEnumerable<T> y listo. Si un sistema de tipo genérico hubiera estado disponible para C# 1.0, es poco probable que la función "hacer coincidir el patrón" se haya implementado en primer lugar.

Como usted ha señalado, en lugar de buscar el patrón, el código generado para una colección dinámica en un foreach busca una conversión dinámica de IEnumerable (y luego hace una conversión del objeto devuelto por la corriente a la tipo de la variable de bucle, por supuesto). Así que su pregunta básicamente es "¿por qué el código generado por el uso del tipo dinámico como un tipo de colección de foreach no puede buscar el patrón en tiempo de ejecución?"

Porque ya no es 1999, e incluso cuando estaba de regreso en el C# 1.0 días, las colecciones que usaron el patrón también casi siempre implementaron IEnumerable también. La probabilidad de que un usuario real escriba código de calidad de producción C# 4.0 que hace un foreach sobre una colección que implementa el patrón pero no IEnumerable es extremadamente baja. Ahora, si estás en esa situación, bueno, eso es inesperado, y lamento que nuestro diseño no haya podido anticipar tus necesidades. Si considera que su escenario es de hecho común, y que hemos calculado mal qué tan raro es, por favor publique más detalles acerca de su escenario y consideraremos cambiar esto para versiones futuras hipotéticas.

Tenga en cuenta que la conversión que generamos en IEnumerable es una conversión dinámica, no simplemente una prueba de tipo. De esta forma, el objeto dinámico puede participar; si no implementa IEnumerable pero desea ofrecer un objeto proxy que sí lo haga, es libre de hacerlo.

En resumen, el diseño de "foreach dinámico" es "solicitar dinámicamente al objeto una secuencia IEnumerable", en lugar de "hacer dinámicamente cada operación de prueba de tipo que habríamos hecho en tiempo de compilación". En teoría, esto viola sutilmente el principio de diseño de que el análisis dinámico da el mismo resultado que el análisis estático, pero en la práctica es así como esperamos que funcione la gran mayoría de las colecciones accedidas dinámicamente.

+0

1. ¡Gracias por tu respuesta, Eric! :) 2. La función "combinar el patrón" no es solo para "mundo antes de los genéricos", sino que también se usa hoy en día, por ejemplo, por tipo genérico ** fundamental como tipo 'List '(y las razones para esto es lo mismo - beneficio de rendimiento). 3. Tengo entendido que la característica "combinar con el patrón" es muy poco común hoy en día y no quiero que C# lo admita, en primer lugar, solo se trata de hacer coincidir los comportamientos dinámico y estático. – ControlFlow

+0

4. ¡Me importa ignorar por completo el 'IEnumarable ' hoy en día! Preferiré que el diseño de "foreach dinámico" sea "solicitar dinámicamente al objeto una secuencia de IEnumerable si la instrucción foreach define el tipo T de preguntar a la secuencia IEnumerable de lo contrario". Esto estará mucho más cerca del comportamiento estático y el resultado con un mejor rendimiento. El único problema es que las colecciones solo implementan 'IEnumerable', lo cual es raro hoy en día y puede ser manejado por' CSharpConvertBinder'. – ControlFlow

+0

Lista @ControlFlow no utiliza "coincida con el patrón", ya que implementar IEnumerable directamente: Lista public class : IList , ICollection , IEnumerable , IList, ICollection, IEnumerable –

2

¿Pero por qué debería escribir código como este?

Indeed. ¿Y por qué el compilador escribiría un código así? Has eliminado cualquier posibilidad de que tuviese que adivinar que el bucle podría optimizarse. Por cierto, parece que interpretas incorrectamente el IL, se vuelve a enlazar para obtener IEnumerable.Current, la llamada MoveNext() es directa y GetEnumerator() se llama solo una vez. Lo cual creo que es apropiado, el siguiente elemento podría o no lanzarse a un int sin problemas. Podría ser una colección de varios tipos, cada uno con su propia carpeta.