[C#]–Enumeraciones y corutinas

¡Buenas!

Empezamos con una pregunta:

¿Cual es el resultado de este programa?

class Program

{

    static void Main(string[] args)

    {

        var data = Foos;

        foreach (var foo in data)

        {

            ChangeFooValue(foo);

        }

 

        var firstFoo = data.First();

        Console.WriteLine(firstFoo.Value);

        Console.ReadLine();

    }

 

    private static IEnumerable<Foo> Foos

    {

        get

        {

            for (var idx = 1; idx <= 10; idx++)

            {

                yield return new Foo(idx);

            }

        }

    }

 

    private static void ChangeFooValue(Foo foo)

    {

        foo.Value = foo.Value + 10;

    }

}

 

internal class Foo

{

    public int Value { get; set; }

    public Foo(int i)

    {

        Value = i;

    }

}

¿Ya lo has meditado?

Pues ahora la solución…

Aunque el sentido común te pueda decir que el valor que se imprimirá es 11, el valor que realmente se imprime es 1.

¿Y eso? A priori todo parece claro: Tenemos una propiedad llamada Foos que nos devuelve 10 objetos Foo (con valores del 1 al 10). Nos guardamos dichos valores en data, iteramos sobre ellos y añadimos 10 al valor de cada objeto Foo. Luego imprimimos el valor del primero de esos objetos. Todo está perfecto, salvo que el resultado es 1 y no 11 como debería ser.

La clave es en como está definida la propiedad Foo (usando yield return) lo que impacta directamente en lo que es la variable data. Por qué… ¿Qué es data? ¿Es una lista? ¿Es un array? ¿Alguien lo sabe?

Cuando usamos yield return no se crea un espacio de almacenamiento para guardar los valores que vamos devolviendo. Se van creando uno tras otro tantas veces como se necesita. Realmente data no contiene ningún objeto (la frase que he usado antes de “Nos guardamos dichos valores en data” es totalmente inexacta), es como un apuntador a “una colección virtual (de 10 elementos)”. Por eso cuando iteramos con el foreach pasamos 10 veces por el yield return y cuando luego usamos el .First() pasamos otra vez más por el yield return. Aunque antes en el foreach se han recuperado los 10 elementos de Foos, como no están guardados en ningún sitio, al hacer .First() se vuelve a recuperar el primero. Lo que crea un Foo nuevo cuyo valor es 1. De ahí el resultado final.

Si usas Resharper, que sepas que te va avisar:

image

Este aviso, simplemente te indica que se está recorriendo más de una vez el mismo IEnumerable (en este ejemplo se recorre una vez con el foreach y otra vez al usar .First()). Recorrer dos veces un IEnumerable puede tener consecuencias no esperadas o puede ser perfectamente correcto. Como Resharper no tiene manera de saberlo, te avisa, para que le eches un vistazo.

Esto es así tan solo si en algún momento NO se guardan en ningún sitio los elementos recuperados. Basta con hacer:

var data = Foos.ToList();

Ahora el resultado final es 11, ya que el método .ToList() copia los valores a una List<T> por lo que ahora data si que tiene almacenados los 10 valores Foo. Y cuando hacemos data.First() recuperamos simplemente el primer valor almacenado en data.

Un caso real…

Bien, probablemente pienses que esto tampoco tiene mucha importancia porque tu no vas por ahí haciendo “colecciones virtuales” con yield, pero que sepas que hay alguien que si que lo hace: alguien llamado EF. En un proyecto en el que estuve nos encontramos con este comportamiento precisamente con el uso de EF: Usábamos EF para recuperar ciertos registros de la BBDD que luego convertíamos a DTOs con un método extensor que dado un IEnumerable<T> devolvía un IEnumerable<TDto>. Dicho método extensor iteraba sobre el IEnumerable<T> original y devolvia otro IEnumerable de Dtos:

public static IEnumerable<R> Map<T, R>(this IEnumerable<T> source)

{

    foreach (var t in source)

    {

        yield return Mapper.Map<T, R>(t);

    }

}

(En nuestro caso la clase Mapper era AutoMapper).

A priori parece todo correcto: Obtenemos datos de EF y los mapeamos a DTOs. Si nos limitamos a devolver el IEnumerable obtenido por Map<T,R> todo va perfecto. El problema es si modificamos alguno de los DTOs que hemos obtenido. Cuando volvemos a iterar sobre la variable que contiene los resultados volvemos a obtener los resultados originales. Eso es debido a que EF crea una colección virtual (es decir usa yield return) para devolvernos los resultados y nuestro Map<T,R> hace lo mismo, por lo que en ningún momento hemos “guardado” esos datos en ningún sitio. Estamos en la misma situación que el ejemplo inicial de este post.

La solución pasa por materializar (es decir llamar a .ToList() o .ToArray()) el resultado devuelto por EF.

Para finalizar solo comentar que el uso de yield return en C# es un ejemplo de lo que se conoce como “corutina”. Una corutina no deja de ser una subrutina (es decir una función o pedazo de código) pero que es reentrante en distintos puntos. Otro ejemplo de corutinas en C# lo puedes encontrar en el uso de la palabra clave await (en C# 5).

¡Un saludo!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *