index array c# foreach language-implementation language-specifications

array - foreach c# list



¿Cómo se implementa foreach en C#? (2)

No usa una función anónima, no. Básicamente, el compilador convierte el código en algo ampliamente equivalente al bucle while que se muestra aquí.

foreach no es una llamada de función, está incorporada en el propio lenguaje, al igual que for bucles y los bucles while. No hay necesidad de devolver nada o "tomar" una función de ningún tipo.

Tenga en cuenta que foreach tiene algunas arrugas interesantes:

  • Cuando se recorre una matriz (conocida en tiempo de compilación), el compilador puede usar un contador de bucle y comparar con la longitud de la matriz en lugar de usar un IEnumerator
  • foreach eliminará el iterador al final; eso es simple para IEnumerator<T> que se extiende a IDisposable , pero como IEnumerator no lo hace , el compilador inserta una verificación para probar en el momento de la ejecución si el iterador implementa IDisposable
  • Puede iterar sobre los tipos que no implementan IEnumerable o IEnumerable<T> , siempre que tenga un método GetEnumerator() aplicable que devuelva un tipo con los miembros Current y MoveNext() adecuados. Como se señaló en los comentarios, un tipo también puede implementar IEnumerable o IEnumerable<T> explícitamente, pero tiene un GetEnumerator() público GetEnumerator() que devuelve un tipo diferente a IEnumerator / IEnumerator<T> . Consulte la List<T>.GetEnumerator() para ver un ejemplo: esto evita la creación innecesaria de un objeto de tipo de referencia en muchos casos.

Consulte la sección 8.8.4 de la especificación C # 4 para obtener más información.

Esta pregunta ya tiene una respuesta aquí:

¿Cómo se implementa exactamente foreach en C #?

Me imagino una parte de ella pareciendo:

var enumerator = TInput.GetEnumerator(); while(enumerator.MoveNext()) { // do some stuff here }

Sin embargo, no estoy seguro de lo que realmente está pasando. ¿Qué metodología se usa para devolver el enumerator.Current ? ¿ enumerator.Current para cada ciclo? ¿Devuelve [para cada ciclo] o toma una función anónima o algo para ejecutar el cuerpo de foreach ?


Sorprendido la aplicación exacta no se toca. Si bien lo que ha publicado en la pregunta es el formulario más simple, la implementación completa (incluida la eliminación del enumerador, la conversión, etc.) se encuentra en la sección 8.8.4 de la especificación.

Ahora hay 2 escenarios donde se puede ejecutar un bucle foreach en un tipo:

  1. Si el tipo tiene un método público / no estático / no genérico / sin parámetros llamado GetEnumerator que devuelve algo que tiene un método público MoveNext y una propiedad Current pública. Como lo señaló el Sr. Eric Lippert en este artículo del blog , esto fue diseñado para adaptarse a la era pre genérica tanto para la seguridad de tipos como para los problemas de rendimiento relacionados con el boxeo en el caso de los tipos de valor. Tenga en cuenta que este es un caso de escribir pato . Por ejemplo, esto funciona:

    class Test { public SomethingEnumerator GetEnumerator() { } } class SomethingEnumerator { public Something Current //could return anything { get { return ... } } public bool MoveNext() { } } //now you can call foreach (Something thing in new Test()) //type safe { }

    Esto es traducido por el compilador a:

    E enumerator = (collection).GetEnumerator(); try { ElementType element; //pre C# 5 while (enumerator.MoveNext()) { ElementType element; //post C# 5 element = (ElementType)enumerator.Current; statement; } } finally { IDisposable disposable = enumerator as System.IDisposable; if (disposable != null) disposable.Dispose(); }

  2. Si el tipo implementa IEnumerable donde GetEnumerator devuelve IEnumerator que tiene un método público MoveNext y una propiedad Current pública. Pero un caso secundario interesante es que incluso si implementa IEnumerable explícitamente (es decir, no hay un método público GetEnumerator en la clase de Test ), puede tener un foreach .

    class Test : IEnumerable { IEnumerator IEnumerable.GetEnumerator() { } }

    Esto se debe a que, en este caso, foreach se implementa como (siempre que no haya otro método público GetEnumerator en la clase):

    IEnumerator enumerator = ((IEnumerable)(collection)).GetEnumerator(); try { ElementType element; //pre C# 5 while (enumerator.MoveNext()) { ElementType element; //post C# 5 element = (ElementType)enumerator.Current; statement; } } finally { IDisposable disposable = enumerator as System.IDisposable; if (disposable != null) disposable.Dispose(); }

    Si el tipo implementa IEnumerable<T> explícitamente, el foreach se convierte a (siempre que no haya otro método público GetEnumerator en la clase):

    IEnumerator<T> enumerator = ((IEnumerable<T>)(collection)).GetEnumerator(); try { ElementType element; //pre C# 5 while (enumerator.MoveNext()) { ElementType element; //post C# 5 element = (ElementType)enumerator.Current; //Current is `T` which is cast statement; } } finally { enumerator.Dispose(); //Enumerator<T> implements IDisposable }

Algunas cosas interesantes a tener en cuenta son:

  1. En los dos casos anteriores, la clase Enumerator debe tener un método público MoveNext y una propiedad Current pública. En otras palabras, si está implementando la interfaz IEnumerator , debe implementarse implícitamente. Por ejemplo, foreach no funcionará para este enumerador:

    public class MyEnumerator : IEnumerator { void IEnumerator.Reset() { throw new NotImplementedException(); } object IEnumerator.Current { get { throw new NotImplementedException(); } } bool IEnumerator.MoveNext() { throw new NotImplementedException(); } }

    (Gracias a Roy Namir por señalar esto. Para cada implementación no es tan fácil como parece en la superficie)

  2. Prioridad del enumerador: parece que si tiene un método public GetEnumerator , esa es la opción predeterminada de foreach independientemente de quién lo esté implementando. Por ejemplo:

    class Test : IEnumerable<int> { public SomethingEnumerator GetEnumerator() { //this one is called } IEnumerator<int> IEnumerable<int>.GetEnumerator() { } }

    Si no tiene una implementación pública (es decir, solo una implementación explícita), la prioridad es como IEnumerator<T> >> IEnumerator .

  3. Hay un operador de foreach involucrado en la implementación de foreach donde el elemento de recopilación se devuelve al tipo (especificado en el propio bucle foreach ). Lo que significa que incluso si hubieras escrito el SomethingEnumerator así:

    class SomethingEnumerator { public object Current //returns object this time { get { return ... } } public bool MoveNext() { } }

    Podrías escribir:

    foreach (Something thing in new Test()) { }

    Debido a que Something es compatible con el tipo, siguiendo las reglas de C #, o en otras palabras, el compilador lo permite si hay una conversión explícita posible entre los dos tipos. De lo contrario el compilador lo impide. La conversión real se realiza en tiempo de ejecución que puede o no puede fallar.