El uso del desconocido yield
Cuando programamos, generalmente tendemos a utilizar las mismas expresiones y manías, dejando a un lado diferentes modos de implementar las mismas soluciones. Puede ser que el uso de expresiones poco conocidas no sea lo más mantenible de cara a no volver locos a nuestros compañeros pero, por otra parte, si un lenguaje nos ofrece ciertas características y funcionalidades, ¿por qué no utilizarlas? Este puede ser el caso de la expresión yield
. En varias ocasiones he notado que no era conocida por compañeros o que en caso de que la conociesen, la evitaban. Lleva con nosotros desde la versión 2.0 de .NET, por lo que tampoco se puede decir que se trate de una novedad. En este artículo trataremos de explicar qué es y para qué se usa.
Empecemos primero por la descripción que nos ofrece Microsoft en MSDN, que dice así:
Cuando se usa la palabra clave contextual yield
en una instrucción, se indica que el método, el operador o el descriptor de acceso get
en el que aparece es un iterador. Al usar yield
para definir un iterador ya no es necesaria una clase adicional explícita (la clase que retiene el estado para una enumeración) al implementar los patrones IEnumerable
y IEnumerator
para un tipo de colección personalizado.
Entendido, ¿verdad? En caso de que sea así, no es necesario seguir leyendo este artículo. En caso contrario, intentaremos aclarar qué es lo que hace este operador desconocido dándole un enfoque diferente y utilizando algún ejemplo simple.
Cómo describía MSDN, yield
es una palabra clave contextual, es decir, una palabra reservada de C# y siempre va acompañada de las palabras clave return
o break
. Cuando se alcanza un yield return
desde el método iterador de un foreach
o una consulta LINQ, el método devolverá un determinado valor y retiene el estado del método antes de devolver el control. Al contrario de un return
normal, en el que se devolvería un valor y se daría por terminado dicho método, con yield
, al volver a lanzarse dicha función, ésta se ejecutará desde el punto en el que se hizo el anterior return
. En caso de comenzar un nuevo foreach
o de realizarse una nueva consulta de LINQ, la ejecución del método que contiene el yield return
volvería a ejecutarse desde el principio, es decir, el estado del método solo se guarda hasta que haya finalizado el bloque en el que se ejecuta la función iterador.
Para poder verlo de una forma más clara, supongamos que queremos calcular los números pares dentro de un rango de enteros. Una aproximación "tradicional" podría ser la siguiente:
static void Main(string[] args)
{
Console.WriteLine("Cálculo de números pares.");
var startingNumber = 10;
var finishingNumber = 30;
var result = new List<int>();
for (int i = startingNumber; i <= finishingNumber; i++)
{
if (i % 2 == 0) result.Add(i);
}
Console.WriteLine($"Los números pares entre {startingNumber} y {finishingNumber} son: {string.Join(", ", result)}");
Console.ReadLine();
}
En este ejemplo, se define un numero inicial, uno final, y se lanza un bucle en el que se hace el cálculo de los números pares presentes en dicho rango. Veamos un ejemplo en el que se utiliza un método iterador con yield return
para hacer el mismo cálculo:
static void Main(string[] args)
{
var startingNumber = 10;
var finishingNumber = 30;
Console.WriteLine("Cálculo de números pares con yield return.");
var resultWithYield = new List<int>();
foreach (var evenNumber in GetEvenNumbers(startingNumber, finishingNumber))
{
resultWithYield.Add(evenNumber);
}
Console.WriteLine($"Los números pares entre {startingNumber} y {finishingNumber} son: {string.Join(", ", resultWithYield)}");
Console.ReadLine();
}
public static IEnumerable<int> GetEvenNumbers(int startingNumber, int finishingNumber)
{
for (int i = startingNumber; i <= finishingNumber; i++)
{
if (i % 2 == 0) yield return i;
}
}
En el anterior bloque de código, sería el método GetEvenNumbers
el encargado de calcular los números pares. Al haber sido llamado desde un foreach
, las instrucciones se ejecutarían hasta el yield return
devolviendo al bloque del foreach
un número par en cada llamada.
La instrucción yield break
se utiliza para indicar que no hay más elementos a ser devueltos, por ejemplo, en una comprobación simple de que un parámetro proporcionado sea nulo:
public IEnumerable<int> GetEvenNumbers(IEnumerable<int> numbers)
{
if (numbers==null) yield break;
foreach(var candidateNumber in numbers)
{
if (candidateNumber %2 == 0) yield return candidateNumber;
}
}
Consideraciones a tener en cuenta:
- No se puede usar la expresión
yield return
en un bloquetry-catch
, pero sí que se puede tener en un bloquetry-finally
. - No se puede usar la expresión
yield break
dentro de un bloquefinally
. - El tipo de retorno de los métodos en los que se usa
yield
debe serIEnumerable
,IEnumerable<T>
,IEnumerator
oIEnumerator<T>
. - En los métodos que se usa
yield
no pueden tener parámetros de tiporef
uout
. - No se pueden usar las expresiones
yield return
oyield break
dentro de métodos anónimos. - No se pueden usar las expresiones
yield return
oyield break
dentro de métodos "unsafe".
La primera conclusión derivada del uso de yield
es que, generalmente, contribuye a escribir un código más eficiente y de mayor facilidad de lectura. En el ejemplo mostrado, el cálculo de números pares queda claramente separado del resto de la lógica, que aunque en este caso sea algo muy simple y probablemente innecesario, en un caso real puede tener verdadera importancia.
La segunda es que no es tan complicado como puede parecer en primera instancia. Se trata de una expresión que tenemos disponible en el framework desde hace ya mucho tiempo y no existe una verdadera razón para no querer entenderla y comenzar a utilizarla. ¡Animaos a probarla y disfrutad de un código más claro!