Qué es yield y por qué hay que usarlo

Seguro que alguna vez has utilizado en C# la palabra clave yield. Se usa para indicar al compilador que estamos dentro de un bloque de iteración y nos permite acceder a los elementos de una lista (IEnumerable) de forma progresiva, a medida que los vamos necesitando. Vamos a ver qué ventajas nos reporta y en qué se traduce cuando compilamos.

Empezaré implementando un mismo método de dos formas diferentes: devolviendo una lista al uso y utilizando yield.

        public static IEnumerable<Product> GetProducts()
        {
            using (var repo = new Repository())
            {
                var products = from product in repo.Product
                               select product;

                return products.ToList();
            }
        }

        public static IEnumerable<Product> GetProducts()
        {
            using (var repo = new Repository())
            {
                var products = from product in repo.Product
                               select product;

                foreach (var product in products)
                {
                    yield return product;
                }
            }
        }

En ambos casos obtenemos la lista de productos desde nuestro repositorio, pero con una gran diferencia: en el primer método obtenemos la lista completa antes de devolverla, mientras que en el segundo los elementos se van obteniendo a medida que lo necesite quien nos llame. Esto es importante por varios motivos:

  • La mayoría de las veces obtener toda la lista a priori será un desperdicio. Imagina que hay miles de registros y el método que nos llama sólo ha de acceder a los primeros. Por ejemplo, pensemos en los métodos de LINQ (como Take()).
  • Obtener una lista muy larga de valores completamente consume mucho tiempo. Si estamos mostrando un listado a un usuario, éste deberá esperar a que se obtenga todos los elementos antes de ver nada… ¡incluso aunque en la UI paginemos el resultado!

En cualquier caso, siempre podemos materializar la lista completa antes de utilizarla llamando a ToList() ¡Todo son ventajas con yield!

En el siguiente ejemplo vemos claramente el orden en que se va ejecutando la lógica de iteración, y las diferencias al hacerlo de una u otra manera:

    class Program
    {
        public static IEnumerable<int> TestEnum()
        {
            for (var i = 0; i < 4; i++)
            {
                Console.Write("yield ");
                yield return i;
            }
        }

        static void Main(string[] args)
        {
            Console.WriteLine("Entera con yield: ");
            foreach (var i in TestEnum())
                Console.Write("{0} ", i);

            Console.WriteLine("\nHasta 2 completa: ");
            var lFullList = TestEnum().ToArray();
            foreach (var i in lFullList)
            {
                Console.Write("{0} ", i);
                if (i == 2) break;
            }

            Console.WriteLine("\nHasta 2 con yield: ");
            foreach (var i in TestEnum())
            {
                Console.Write("{0} ", i);
                if (i == 2) break;
            }
        }
    }

    // Salida:
    // Entera con yield:
    // yield 0 yield 1 yield 2 yield 3
    // Hasta 2 completa:
    // yield yield yield yield 0 1 2
    // Hasta 2 con yield:
    // yield 0 yield 1 yield 2

Como vemos, en el último caso sólo se accede al valor tres veces, mientras que si materializamos la lista se accede las 4 veces y por adelantado. Adivinanza: ¿Cuál sería salida si llamamos a TestEnum().Take(3)?🙂

¿Y qué pasa cuando compilamos? Según la MSDN:

El compilador genera una clase para implementar el comportamiento que se expresa en el bloque de iteradores. En el bloque de iteradores, la palabra clave yield se usa junto con la palabra clave return para proporcionar un valor al objeto enumerador. Este es el valor que se devuelve, por ejemplo, en cada bucle de una instrucción foreach.

Si tiramos de reflector y abrimos el ejecutable compilado a partir del fuente anterior, vemos que se ha generado una clase privada y sellada a partir de nuestro método TestEnum que implementa IEnumerable, IEnumerable<int>, IEnumerator, IEnumerator<int> e IDisposable. Sólo pego aquí abajo la implementación del método MoveNext, ya que contiene algo que nos sonará😛

        [CompilerGenerated]
        private sealed class _TestEnum_d__0 : IEnumerable, IEnumerable<int>, IEnumerator, IEnumerator<int>, IDisposable
        {
            private int _1__state;
            private int _2__current;
            private int _l__initialThreadId;
            public int _i_5__1;

            /* Más métodos */

            private bool MoveNext()
            {
                switch (this._1__state)
                {
                    case 0:
                        this._1__state = -1;
                        this._i_5__1 = 0;
                        while (this._i_5__1 < 4) // Te suena el 4? ;P
                        {
                            // Esto es el código dentro del for
                            // de nuestro método TestEnum
                            Console.Write("yield ");
                            this._2__current = this._i_5__1;
                            this._1__state = 1;
                            return true;
                        Label_0051:
                            this._1__state = -1;
                            this._i_5__1++;
                        }
                        break;
                    case 1:
                        goto Label_0051;
                }
                return false;
            }
        }

En resumen, yield nos ofrece una forma sencilla de trabajar óptimamente con listas de elementos ahorrándonos los detalles más tediosos de implementar los interfaces corespondientes, ya que el compilador lo hace por nosotros. Las ventajas en cuanto a rendimiento son evidentes, y más aún si hacemos uso de LINQ para trabajar con las listas.

Esta entrada fue publicada en Dev y etiquetada , , , , , . Guarda el enlace permanente.

2 respuestas a Qué es yield y por qué hay que usarlo

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s