Sobre ir recorriendo enumerables…

Publicado 11/6/2010 15:28 por Eduard Tomàs i Avellana

El otro día, Oren Eini (aka Ayende) escribió en su blog un post, en respuesta a otro post escrito por Phil Haack (aka Haacked). En su post Phil mostraba un método extensor para comprobar si un IEnumerable<T> era null o estaba vacío (y sí, Phil usa Any() en lugar de Count() para comprobar si la enumeración está vacía):

public static bool IsNullOrEmpty<T>(this IEnumerable<T> items) {
return items == null || !items.Any();
}

Aquí tenéis el post de Phil: Checking For Empty Enumerations

Y este es el de Oren: Checking For Empty Enumerations

Oren plantea una cuestión muy interesante al respecto de los enumerables y es la posibilidad de que haya enumeraciones que sólo se puedan recorrer una sóla vez. En este caso el método de Phil fallaría, puesto que al llamar a .Any() para validar si hay un elemento este elemento sería leído (y por lo tanto se perdería) por lo que cuando después recorriesemos el enumerable no obtendríamos el primer elemento.

Pero el tema es… ¿pueden existir enumerables de un sólo recorrido? Pues poder, pueden pero mi opinión personal es que no son enumerables válidos.

Vayamos por partes… Que es un enumerable? Pues algo tan simple como esto:

public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}

Nota: Si no os suena eso de “out” es una novedad de C# 4 que nos permite especificar covarianza en el tipo genérico. No afecta a lo que estamos discutiendo en este post. Tenéis más info en este clarificador post de Matt Hiddinger.

Bueno… resumiendo lo único que podemos hacer con un enumerable es… obtener un enumerador. Y que es un enumerador? Pues eso:

public interface IEnumerator<out T> : IDisposable, IEnumerator
{
T Current { get; }
// Esto se hereda de IEnumerator
object Current { get; }
bool MoveNext();
void Reset();
// Esto se hereda de IDisposable
void Dispose();
}

Un enumerador es lo que se recorre: Tenemos una propiedad (Current) que nos permite obtener el elemento actual así como un método MoveNext() que debe moverse al siguiente elemento (devolviendo true si este movimiento ha sido válido). Hay otro método adicional Reset() que debe posicionar el enumerador antes del primer elemento, aunque en la propia MSDN se indica que no es un método de obligada implementación. Así pues, ciertamente no podemos asumir que un IEnumerator se pueda recorrer más de una vez. Así que no lo hagáis: asumid que los IEnumerator sólo pueden recorrerse una vez.

Pero que un IEnumerator sólo pueda recorrerse una vez no implica que no pueda obtener dos, tres o los que quiera IEnumerator a partir del mismo IEnumerable: puedo llamar a GetEnumerator() tantas veces como quiera.

Y, ahí está el quid de la cuestión del post de Oren: el método Any() crea un IEnumerator y luego el foreach crea otro IEnumerator. Así pues en este código:

void foo(IEnumerable<T> els)
{
if (els.Any()) {
foreach (var el in els) { ... }
}
}

Se crean dos IEnumerator: uno cuando se llama a Any() y otro cuando se usa el foreach. Y ambos IEnumerators nos permiten recorrer el IEnumerable des del principio: por eso no perdemos ningún elemento (al contrario de lo que afirma Oren en su post).

Conclusión

Antes he dicho que pueden existir IEnumerables que solo se puedan recorrer una sola vez, pero que en mi opinión no son correctos. Cuando digo que pueden existir me refiero a que se pueden crear, cuando digo que (en mi opinión) no son correctos me refiero a que según la msdn (http://msdn.microsoft.com/en-us/library/system.collections.ienumerable.getenumerator.aspx) la llamada a GetEnumerator debe:

  • Devolver un enumerador (IEnumerator) que itere sobre la colección (Returns an enumerator that iterates through a collection).
  • Inicialmente el enumerador debe estar posicionado antes del primer elemento (Initially, the enumerator is positioned before the first element in the collection).

Por lo tanto de aquí yo interpreto que cada vez que llame a GetEnumerator obtendré un enumerador posicionado antes del primer elemento, y dado que en ningún momento se me avisa que un IEnumerable pueda admitir una SOLA llamada a GetEnumerator(), entiendo que puedo obtener tantos enumeradores como quiera y que cada llamada me devolverá un IEnumerator posicionado antes del primer elemento.

Así que podéis usar el método de Phil para comprobar si un IEnumerable está vacío sin miedo a perder nunca el primer elemento!

Un saludo!

Archivado en: ,,
Comparte este post:

Comentarios

# re: Sobre ir recorriendo enumerables…

Monday, June 14, 2010 4:33 PM by Lucas Ontivero

Bueno Eduard, antes que nada dejame decirte que tu blog es buenísimo. Dicho esto, veo que tienes razón en tu planteo. No obstante, eso no le resta validez al argumento Oren. Dejame mostrar mi punto:

Mientras  usas solo un enumerador, la instancia del IEnumerable<> recorrida no puede ser modificada, es decir, si la modificas la próxima llamada al método MoveNext() te lanza una excepción. Por esta razón, no es lo mismo usar el mismo enumerador que usar dos instancias diferentes del mismo porque entre la creación de uno y el otro otro pueden pasar muchas cosas.  

Tampoco, nada impide que un programador cree su propia colección y su enumerador el cual luego de consumir cada elemento, le modifique su estado. En este caso (estamos hilando fino porque tu post hila fino) tenemos un enumerador que aunque pudiera resetearse, solo podemos recorrerlo una solo vez.

Saludos.

# re: Sobre ir recorriendo enumerables…

Wednesday, June 16, 2010 12:25 PM by Eduard Tomàs i Avellana

@Lucas

Muchas gracias por tu comentario! Encantado de que te guste el blog! :)

Antes que nada... tienes razón en todo lo que comentas.

Es cierto que no es lo mismo usar dos enumeradores que la misma instancia, pero si yo se que entre la creación de los dos enumeradores (es decir entre el Any() y el foreach) pueden pasar cosas sobre la colección, también debería saber que entonces no puedo preguntar si hay elementos ANTES y DESPUES recorrer la colección... ya que debería saber que esta puede haber cambiado. No se si me explico.

Y sobre el segundo punto, tienes también razón. Nada te impide hacer esto. De hecho, yo lo hice para probar: un clase que implementaba IEnumerable<string> y que te devolvía enumeradores todos ellos vinculados al mismo stream de un fichero. Resultado: al hacer Any() y luego foreach, perdía la primera línea del fichero, justo lo que menciona Oren en su post...

... lo que yo me cuestiono entonces es: Es esta clase una implementación correcta de IEnumerable<string>?? Yo creo que no.

El post de Oren me pareció muy interesante, no por el hecho de que él pueda tener razón o estar equivocado, sinó porque plantea un ejercicio que muchas veces obviamos: pensar. Pensar si pueden haber colecciones que se recorran una sola vez, pensar si tienen sentido, pensar que podría ocurrir si existiesen... Y todo esto a mi me parece sumamente interesante.

Un abrazo y gracias de nuevo por tu comentario!