viernes, 11 de junio de 2010

Sobre ir recorriendo enumerables…

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!



PD: Esto es un crosspost desde mi blog de geeks.ms. Pásate por allí que somos más!! ;-)

No hay comentarios: