2010-04-29 10 views
6

Por lo tanto, esta cuestión se acaba de preguntar sobre SO:¿Puede alguien explicar este código de evaluación perezosa?

How to handle an "infinite" IEnumerable?

Mi código de ejemplo:

public static void Main(string[] args) 
{ 
    foreach (var item in Numbers().Take(10)) 
     Console.WriteLine(item); 
    Console.ReadKey(); 
} 

public static IEnumerable<int> Numbers() 
{ 
    int x = 0; 
    while (true) 
     yield return x++; 
} 

Por favor alguien puede explicar por qué esto es perezoso evaluar? He buscado este código en Reflector, y estoy más confundido que cuando comencé.

salidas Reflector:

public static IEnumerable<int> Numbers() 
{ 
    return new <Numbers>d__0(-2); 
} 

Para el método de números, y se ve que han generado un nuevo tipo para que la expresión:

[DebuggerHidden] 
public <Numbers>d__0(int <>1__state) 
{ 
    this.<>1__state = <>1__state; 
    this.<>l__initialThreadId = Thread.CurrentThread.ManagedThreadId; 
} 

Esto no tiene sentido para mí. Hubiera supuesto que era un ciclo infinito hasta que reuní ese código y lo ejecuté yo mismo.

EDITAR: Así que entiendo ahora que .Tomar() puede decirle al foreach que la enumeración ha 'terminado', cuando en realidad no tiene, pero no debe Números() se llama en su totalidad antes de encadenamiento reenviar al Take()? El resultado de Take es lo que en realidad se está enumerando, ¿correcto? ¿Pero cómo se está ejecutando Take cuando Numbers no ha evaluado completamente?

EDIT2: Entonces, ¿se trata de un truco de compilación específico impuesto por la palabra clave 'yield'?

Respuesta

1

La razón por la cual no se trata de un bucle infinito es que solo enumera 10 veces de acuerdo con el uso de la llamada Take (10) de Linq. Ahora bien, si usted escribió el código de algo como:

foreach (var item in Numbers()) 
{ 
} 

Ahora bien, este es un bucle infinito debido a que su empadronador siempre devolverá un nuevo valor. El compilador C# toma este código y lo transforma en una máquina de estados. Si su enumerador no tiene una cláusula de guardia para romper la ejecución, entonces la persona que llama debe hacerlo en su muestra.

La razón por la que el código es flojo también es una razón por la cual el código funciona. Essentially Take devuelve el primer artículo, luego su aplicación se consume, luego toma otro hasta que haya tomado 10 elementos.

Editar

En realidad, esto no tiene nada que ver con la adición de toma. Estos se llaman iteradores. El compilador de C# realiza una transformación complicada en su código creando un enumerador fuera de su método. Recomiendo leer sobre él, pero básicamente (y esto puede no ser 100% exacto), su código ingresará el método Numbers que podría visualizar como la iniciación de la máquina de estado.

Una vez que su código alcanza un rendimiento, en esencia dice que los Números() dejan de ejecutar les devuelve este resultado y luego cuando piden el siguiente artículo reanudan la ejecución en la siguiente línea después del rendimiento.

Erik Lippert has a great series en aspectos misceláneos de iteradores

+0

Sí, pero ¿no se debería evaluar por completo a Numbers() para continuar con la llamada de Take? Entiendo que Numbers en sí es un ciclo infinito. ¿Por qué la adición de .Take() detiene repentinamente la evaluación de Numbers en su totalidad? – Tejs

+0

Interesante. Gracias por el enlace! Esa pregunta (y el código) me hizo dar una doble vuelta, ¡y ahora me doy cuenta de que tengo más que aprender sobre los Enumerables! – Tejs

2

Esto tiene que con:

  • Lo que hace IEnumerable cuando ciertos métodos se llaman
  • La naturaleza de la enumeración y la declaración Rendimiento

Cuando enumera sobre cualquier tipo de IEnumerable, la clase le da el próximo artículo que te va a dar. No hace nada para sus artículos, simplemente le da el siguiente artículo. Decide qué va a ser ese artículo. (Por ejemplo, algunas colecciones están ordenadas, otras no. Algunas no garantizan un orden en particular, pero parecen devolverlas siempre en el mismo orden en que las ingresó).

El método de extensión IEnumerable Take() enumerará 10 veces, obteniendo los primeros 10 elementos. Puedes hacer Take(100000000), y te daría muchos números. Pero solo estás haciendo Take(10). Simplemente pregunta Numbers() para el siguiente artículo. . . 10 veces.

Cada uno de esos 10 elementos, Numbers da el siguiente artículo. Para entender cómo, tendrá que leer en la declaración de rendimiento. Es syntactic sugar por algo más complicado. Yield es muy poderoso. (Soy desarrollador de VB y estoy muy molesto porque todavía no lo tengo). No es una función; es una palabra clave con ciertas restricciones. Y hace que definir un enumerador sea mucho más fácil de lo que podría ser.

Otros métodos de extensión IEnumerable siempre iteran a través de cada elemento. Llamar a .AsList lo explotaría. Al usarlo, la mayoría de las consultas LINQ lo explotarían.

+0

Escribir enumeradores personalizados con rendimiento es muy divertido. – JoshBerke

0

Básicamente, su función Numbers() crea un enumerador.
El foreach comprobará, en cada iteración, si el enumerador ha llegado al final, y si no, continuará. Tu enunciador praticular nunca terminará, pero eso no importa. Esto está siendo evaluado de manera relajada.
El Enumerador generará los resultados "en vivo".
Lo que eso significa es que si escribes .Take (3) allí, el ciclo solo se ejecutará tres veces. El enumrator todavía tendría algunos elementos "left" en él, pero no se generarían ya que ningún método los necesita, en este momento.
Si intenta generar todos los números de 0 a infinito como lo implica la función, y los devuelve a la vez, este programa, que solo usa 10 de ellos, sería mucho, mucho más lento. Ese es el beneficio de la evaluación perezosa: lo que nunca se usa nunca se calcula.

Cuestiones relacionadas