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 bloque try-catch, pero sí que se puede tener en un bloque try-finally.
  • No se puede usar la expresión yield break dentro de un bloque finally.
  • El tipo de retorno de los métodos en los que se usa yield debe ser IEnumerable, IEnumerable<T>, IEnumerator o IEnumerator<T>.
  • En los métodos que se usa yield no pueden tener parámetros de tipo ref u out.
  • No se pueden usar las expresiones yield return o yield break dentro de métodos anónimos.
  • No se pueden usar las expresiones yield return o yield 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!