unity c# ienumerable

unity - ienumerator c#



¿Por qué el manejo de errores para IEnumerator.Current es diferente de IEnumerator<T>.Current? (2)

El problema con IEnumerable<T> es que Current es de tipo T En lugar de lanzar una excepción, se devuelve el default(T) (se establece desde MoveNextRare ).

Cuando utiliza IEnumerable no tiene el tipo y no puede devolver un valor predeterminado.

El problema real es que no verifica el valor de retorno de MoveNext . Si devuelve false , no deberías llamar a Current . La excepción está bien. Creo que encontraron más conveniente devolver el default(T) en el caso IEnumerable<T> .

El manejo de excepciones conlleva una sobrecarga, devolver el default(T) no (tanto). Tal vez solo pensaron que no había nada útil para regresar de la propiedad Current en el caso de IEnumerable (no conocen el tipo). Ese problema se "resuelve" en IEnumerable<T> cuando se usa el default(T) .

De acuerdo con este informe de error (gracias Jesse por comentar):

Por razones de rendimiento, la propiedad Actual de los enumeradores generados se mantiene extremadamente simple: simplemente devuelve el valor del campo de respaldo ''actual'' generado.

Esto podría apuntar en la dirección de la sobrecarga del manejo de excepciones. O el paso extra requerido para validar el valor de la current .

De hecho, solo tienen la responsabilidad de foreach , ya que es el principal usuario del enumerador:

La gran mayoría de las interacciones con los enumeradores son en forma de bucles foreach que ya protegen contra el acceso a la corriente en cualquiera de estos estados, por lo que sería inútil quemar ciclos de CPU adicionales en cada iteración para verificar estos estados que casi nadie encontrará .

Pensé que ejecutar el siguiente código para una colección vacía que implementa IEnumerable<T> generaría una excepción:

var enumerator = collection.GetEnumerator(); enumerator.MoveNext(); var type = enumerator.Current.GetType(); // Surely should throw?

Debido a que la colección está vacía, el acceso a IEnumerator.Current no es válido y habría esperado una excepción. Sin embargo, no se lanza ninguna excepción para la List<T> .

Esto está permitido por la documentación de IEnumerator<T>.Current , que indica que la Current no está definida en ninguna de las siguientes condiciones:

  • El enumerador se coloca antes del primer elemento de la colección, inmediatamente después de que se crea el enumerador. Se debe llamar a MoveNext para avanzar el enumerador al primer elemento de la colección antes de leer el valor de Corriente.
  • La última llamada a MoveNext devolvió false, lo que indica el final de la colección.
  • El enumerador se invalida debido a cambios realizados en la colección, como agregar, modificar o eliminar elementos.

(Supongo que "no se puede lanzar una excepción" se puede categorizar como "comportamiento indefinido" ...)

Sin embargo, si haces lo mismo pero usas un IEnumerable en IEnumerable lugar, SÍ obtienes una excepción. Este comportamiento se especifica en la documentación de IEnumerator.Current , que establece:

  • Actual debe lanzar una InvalidOperationException si la última llamada a MoveNext devolvió false, lo que indica el final de la colección.

Mi pregunta es: ¿Por qué esta diferencia? ¿Hay alguna buena razón técnica que desconozco?

Significa que el código de apariencia idéntica puede comportarse de manera muy diferente dependiendo de si usa IEnumerable<T> o IEnumerable , como lo demuestra el siguiente programa (observe cómo el código dentro de showElementType1() y showElementType1() es idéntico):

using System; using System.Collections; using System.Collections.Generic; namespace ConsoleApplication2 { class Program { public static void Main() { var list = new List<int>(); showElementType1(list); // Does not throw an exception. showElementType2(list); // Throws an exception. } private static void showElementType1(IEnumerable<int> collection) { var enumerator = collection.GetEnumerator(); enumerator.MoveNext(); var type = enumerator.Current.GetType(); // No exception thrown here. Console.WriteLine(type); } private static void showElementType2(IEnumerable collection) { var enumerator = collection.GetEnumerator(); enumerator.MoveNext(); var type = enumerator.Current.GetType(); // InvalidOperationException thrown here. Console.WriteLine(type); } } }


Para igualar mejor cómo las personas tienden a implementarlo en la práctica. Al igual que el cambio de texto de "Actual también lanza una excepción ..." en versiones anteriores de la documentación a "Actual debería lanzar ..." en la versión actual.

Dependiendo de cómo esté funcionando la implementación, lanzar una excepción puede ser un poco de trabajo y, sin embargo, debido a la forma en que se utiliza Current junto con MoveNext() , ese estado excepcional casi nunca aparecerá. Esto es aún más importante cuando consideramos que la gran mayoría de los usos son generados por compiladores y en realidad no tienen alcance para un error al que se llama a Current antes de MoveNext() o después de que haya devuelto false para que alguna vez ocurra. Con el uso normal podemos esperar que el caso nunca se presente.

Por lo tanto, si está escribiendo una implementación de IEnumerable o IEnumerable<T> en la que es difícil detectar la condición de error, puede decidir no hacerlo. Y si toma esa decisión, probablemente no le cause ningún problema. Sí, rompiste las reglas, pero probablemente no importó.

Y ya que no causará ningún problema, excepto por alguien que usa la interfaz de manera incorrecta, documentarla como un comportamiento indefinido traslada la carga del implementador a la persona que llama para no hacer algo que la persona que llama no debería hacer en primer lugar. .

Pero dicho todo esto, ya que IEnumerable.Current todavía está documentado como "debería lanzar InvalidOperationException por compatibilidad con versiones anteriores y, al hacerlo, coincidiría con el comportamiento" indefinido "de IEnumerable<T>.Current , probablemente la mejor manera de cumplir perfectamente el comportamiento documentado de la interfaz es tener IEnumerable<T>.Current lanzar una InvalidOperationException en tales casos, y tener IEnumerable.Current solo llamar a eso.

En cierto modo, esto es contrario al hecho de que IEnumerable<T> también hereda de IDisposable . Los usos generados por el compilador de IEnumerable comprobarán si la implementación también implementa IDisposable y llamará a Dispose() si lo hace, pero aparte de la ligera sobrecarga de rendimiento de esa prueba, esto significaba que tanto los implementadores como los llamadores codificados a mano a veces se olvidaban de eso, y no implementar o llamar a Dispose() cuando deberían. Forzar a todas las implementaciones a tener al menos un Dispose() hecho la vida más fácil para las personas de la manera opuesta a tener un comportamiento indefinido de Current cuando no es válido.

Si no hubiera problemas de compatibilidad con versiones anteriores, probablemente tendríamos Current documentado como no definido en tales casos para ambas interfaces, y ambas interfaces IDisposable de IDisposable . Probablemente tampoco tendríamos Reset() que no es más que una molestia.