La palabra clave yield


La semana pasada el maestro Eduard Tomás propuso la solución del reto MSDN utilizando la palabra clave yield en un método para evitar el uso de una colección adicional.

¿Qué se esconde tras esta palabra clave?

El patrón Iterator

Si has escrito código C# alguna vez, ya conocerás las interfaces IEnumerable e IEnumerable<T> que permiten recorrer fácilmente una colección con la palabra clave foreach:

IEnumerable<int> list=new int[]{1,2,3,4};
//...
foreach (var value in list)
{
    //do something
}

Como veis en el ejemplo, este interfaz lo implementan todas las colecciones desde los tipos más básicos como el Array (aunque de una forma un tanto particular).

El interfaz sólo tiene un método, GetEnumerator que devuelve un IEnumerator, que nos devolverá una clase que es la que realmente sabe iterar sobre nuestra colección de elementos. Es el típico patrón Iterator del GoF (Design Patterns: Elements of Reusable Object-Oriented Software).

La palabra clave yield

Si queremos devolver una colección de elementos, normalmente utilizaremos una colección del sistema como List<T> o similares. En casos más avanzados haremos una propia, en esos casos puede que tengamos que crear el iterador, pero muchas veces será más importante devolver un listado de elementos que se pueda recorrer que la implementación de la colección en sí.

La palabra clave yield nos facilitará la creación de colecciones personalizadas e iteradores sin tener que escribir el conjunto de clases personalizadas para ello. Como con un ejemplo se ve todo más claro, aquí tenéis una colección enumerable que contiene 4 enteros:

public class CollectionWithYield:IEnumerable
{
    public IEnumerator GetEnumerator()
    {
        yield return 1;
        yield return 2;
        yield return 3;
        yield return 4;
    }
}

A partir de aquí la podemos utilizar como cualquier otra colección enumerable:

foreach(var value in new CollectionWithYield())
{
   Console.WriteLine(value);
}

¿Qué hace el compilador cuando encuentra un yield?

La palabra yield va bastante más allá de la típica Syntatic Sugar, pues genera internamente una clase que implementa IEnumerator para nosotros. Aquí tenéis el código descompilado con dotPeek:

public class CollectionWithYield : IEnumerable
{
    public CollectionWithYield()
    {
        base..ctor();
    }

    public IEnumerator GetEnumerator()
    {
        CollectionWithYield.<GetEnumerator>d__0 getEnumeratorD0 = new CollectionWithYield.<GetEnumerator>d__0(0);
        getEnumeratorD0.<>4__this = this;
        return (IEnumerator) getEnumeratorD0;
    }

    [CompilerGenerated]
    private sealed class <GetEnumerator>d__0 : IEnumerator<object>, IEnumerator, IDisposable
    {
        private object <>2__current;
        private int <>1__state;
        public CollectionWithYield <>4__this;

        object IEnumerator<object>.Current
        {
            [DebuggerHidden] get
            {
                return this.<>2__current;
            }
        }

        object IEnumerator.Current
        {
            [DebuggerHidden] get
            {
                return this.<>2__current;
            }
        }

        [DebuggerHidden]
        public <GetEnumerator>d__0(int <>1__state)
        {
            base..ctor();
            this.<>1__state = param0;
        }

        bool IEnumerator.MoveNext()
        {
            switch (this.<>1__state)
            {
                case 0:
                    this.<>1__state = -1;
                    this.<>2__current = (object) 1;
                    this.<>1__state = 1;
                    return true;
                case 1:
                    this.<>1__state = -1;
                    this.<>2__current = (object) 2;
                    this.<>1__state = 2;
                    return true;
                case 2:
                    this.<>1__state = -1;
                    this.<>2__current = (object) 3;
                    this.<>1__state = 3;
                    return true;
                case 3:
                    this.<>1__state = -1;
                    this.<>2__current = (object) 4;
                    this.<>1__state = 4;
                    return true;
                case 4:
                    this.<>1__state = -1;
                break;
            }
            return false;
        }

        [DebuggerHidden]
        void IEnumerator.Reset()
        {
            throw new NotSupportedException();
        }

        void IDisposable.Dispose()
        {
        }
    }
}

Fijaos que el método Reset no está implementado, esto nos lleva al siguiente punto.

Algunos usos de yield… y algunas pegas

Como hemos visto, yield genera código automáticamente, eso significa que nos ahorrará mucho trabajo, pero también que tenemos que ir con cuidado. Algunas veces habrá funcionalidad no cubierta por el generador de código.

Por ejemplo, nos vendrá muy bien cuando tengamos que devolver una lista de elementos infinita, algo que no podríamos hacer con un simple List<T>:

public int GeneratedRandoms { get; private set; }

Random _r = new Random();

public IEnumerable<int> RandomGenerator()
{
    GeneratedRandoms++;
    yield return _r.Next();
}

Pero tendremos que ir con cuidado, pues una implementación así puede generar algunos problemas. LINQ to Objects no nos funcionará demasiado bien, por ejemplo el método de extensión Count() nos devolverá siempre 1 o tras usar Skip(n) una operación de lectura nos provocará una excepción:

[Fact]
public void Custom_Enumeration_Does_Not_Support_Skip()
{
    var coll = _yieldDemo.RandomGenerator();

    Assert.Throws<InvalidOperationException>(() =>
    {
        var intermediate = coll.Skip(5);
        var value=intermediate.First();
        Assert.Equal(6, _yieldDemo.GeneratedRandoms);
    });
}

Para que lo anterior funcione, nuestro código debe tener algún tipo de control de elementos, como un contador:

public IEnumerable<int> NConsecutiveNumbers(int count)
{
    int i = 0;
    while (i < count)
    {
        yield return i++;
    }
}

Esto hará que el yield pueda generar un iterador que se lleve mejor con LINQ.

Otro uso interesante de yield está en “mejorar” o modificar una lista existente en el momento de recorrerla. Por ejemplo, podemos insertar elementos de inicio y de fin en una lista cualquiera:

public IEnumerable<string> EnumerableStrings(IEnumerable<string> list)
{
    yield return "first";
    foreach (var element in list)
    {
        yield return element;
    }
    yield return "last";
}

O bien ir saltando puestos en la lista según nos convenga:

public IEnumerable<int> AllPositiveEven()
{
    int value = 0;
    while (value < int.MaxValue)
    {
        yield return value;
        value += 2;
    }
}

Además de las listas infinitas, también podemos hacerlas cíclicas:

public IEnumerable<string> CycleStrings(IEnumerable<string> values)
{
    if (values == null || values.Count() == 0)
        yield break;

    var enumerator = values.AsEnumerable().GetEnumerator();
    while (true)
    {
        while (enumerator.MoveNext())
        {
            yield return enumerator.Current;
        }
        enumerator.Reset();
    }
}

Aunque parezca una tontería, puede servirnos para muchas cosas, como por ejemplo realizar una animación cíclica:

int x = 40, y = 10;
Task.Run(() =>
{
    YieldDemos demo = new YieldDemos();
    var strings = demo.CycleStrings(new string[] { "|", "/", "-", "\\" });

    foreach (var str in strings)
    {
        Console.Clear();
        Console.SetCursorPosition(x, y);
        Console.Write(str);
        System.Threading.Thread.Sleep(90);
    }
});

while (true)
{
    var key = Console.ReadKey();
    bool exit = false;
    switch (key.Key)
    {
        case ConsoleKey.UpArrow:
            if(y>0) y--;
            break;
        case ConsoleKey.DownArrow:
            if (y < 24) y++;
            break;
        case ConsoleKey.LeftArrow:
            if (x >0) x--;
            break;
        case ConsoleKey.RightArrow:
            if (x < Console.BufferWidth-1) x++;
            break;
        case ConsoleKey.Enter:
            exit=true;
            break;
    }
    if (exit) break;
}

Aunque cuidado, no se os ocurra hacer un Count de una lista cíclica 😉

Enlaces interesantes

Anuncios

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