First vs Single
¡Hola a todos! Linq se ha convertido en nuestro mejor amigo cuando trabajamos con colecciones de datos. Pese a la comodidad que nos ofrece, Linq no es solo azúcar sintáctico: es mucho más, pero ¿siempre usamos Linq correctamente?
Hoy vamos a comparar Single
y First
para ver las diferencias y si los usamos correctamente.
Nota: Para hacer la comparativa vamos a utilizar tanto en el método Single
como en el First
la sobrecarga que recibe un predicado con una condición para buscar el elemento.
Principales diferencias
La principal diferencia es que Single
lanzará una excepción si existe más de un ítem en la colección que cumpla la condición del predicado.
Esto ya nos está indicando que Single
, por norma general, realizará más iteraciones sobre la colección porque no le basta con encontrar un ítem que cumpla el predicado, sino que tiene que seguir hasta encontrar otro ítem que cumpla la condición, para lanzar la excepción, o hasta llegar al final de la colección.
First
únicamente devuelve el primer ítem que encuentre que cumple con el predicado. En ese momento dejará de iterar sobre los elementos de la colección.
Por suerte Linq es Open Source y lo tenemos disponible en GitHub para poder ver las entrañas y comprender mejor cómo funciona. Vamos a ver la implementación de estos dos métodos.
First
He extraído parte del código de la familia de métodos de extensión de First
de GitHub para poder analizarlo mejor.
foreach (TSource element in source)
{
if (predicate(element))
{
found = true;
return element;
}
}
La parte del First
que se encarga de encontrar el primer ítem que cumpla con el predicado no es más que un foreach
que evalúa el predicado y en caso de cumplirse devuelve el ítem.
Por tanto, en cuando haya encontrado el ítem dejará de iterar sobre la colección y devolverá el resultado. Por otro lado, si no encuentra el ítem lanzará una excepción en el caso de First
o default(T)
en el caso de FirstOrDefault
.
Single
El código de Single
es un poco más complejo. Para empezar, trabajamos con el Enumerator
que nos va a permitir navegar por la colección.
using (IEnumerator<TSource> e = source.GetEnumerator())
{
while (e.MoveNext())
{
TSource result = e.Current;
if (predicate(result))
{
while (e.MoveNext())
{
if (predicate(e.Current))
{
throw Error.MoreThanOneMatch();
}
}
return result;
}
}
}
Nota: El método e.MoveNext()
avanzará en la colección y devolverá true
en caso de que haya podido realizarse y false
en caso contrario, es decir, no quedan ítems en la colección.
Por tanto, lo que tenemos es un mientras queden ítems en la colección que evalúa el predicado a ver si se cumple. En el caso de que se cumpla entramos en otro mientras queden ítems en la colección que de nuevo evalúa el predicado. En este caso si se cumple lanzamos una excepción, ya que hemos encontrado más de un ítem.
En caso de no haber encontrado otro elemento que cumpla el predicado devolvemos el que ya habíamos encontrado.
¿Cuál utilizar?
Como hemos podido comprobar First
, por normal general, ejecuta menos iteraciones sobre la colección.
En un caso habitual en el que el predicado sólo lo cumpla un ítem, First
recorrerá la colección hasta que lo encuentre, mientras que Single
recorrerá toda la colección para asegurar que solo un ítem cumple el predicado.
Es evidente que si no tenemos necesidad de comprobar que el predicado solo lo cumple un elemento la mejor opción es utilizar First
que va a iterar sobre la lista sólo hasta que encuentre el ítem.
Bonus Track: Find
Existe otro método, disponible para las listas, llamado Find
que también acepta un predicado. Podemos consultar el código de List
en Github.
public T Find(Predicate<T> match)
...
for (int i = 0; i < _size; i++)
{
if (match(_items[i]))
{
return _items[i];
}
}
return default(T);
Como vemos, Find
utiliza un for
para recorrer la colección, a diferencia de First
que utilizaba un foreach
. Por tanto, podemos concluir que Find
es más rápido recorriendo los ítems, pero no podemos utilizarlo en todo tipo de colecciones.
Nota: Además, si Find
no encuentra ningún ítem que cumpla con el predicado, devolverá default(T)
, teniendo el mismo comportamiento que FirstOrDefault
.
Conclusiones
Hemos comprobado que es mejor utilizar First
que Single
salvo que queramos asegurarnos que no existen más ítems que cumplen el predicado. Además, lo hemos comprobado leyendo el código de Linq.
Es interesante tener disponible el código de Linq porque nos ayuda a comprender mejor los diferentes métodos de extensión que nos ofrece para trabajar con las colecciones.
Escribí este post para investigar y sopesar si era verdad que no estaba utilizando Single
en ocasiones que tendría que haberlo utilizado y creo que la respuesta es sí. En algunos desarrollos tendría que haberlo añadido y, si es el caso, controlar la excepción que lanza para ver cómo actuar al respecto.
Referencias
Free Vector Graphics by Vecteezy.com
¡Nos vemos en el futuro!