воскресенье, 6 января 2019 г.

Синтаксический сахар yield return

На stackoverflow задали вопрос про yield return. Ну и т.к. пример для понимания что это за зверь написал, то пусть он и в этом блоге полежит. Под катом два примера, как реализовать класс реализующий IEnumerable или IEnumerable классически и при помощи yield return. Для тех кто не в курсе, foreach и методы Linq работают с классами реализующими эти интерфейсы.

Итак, давайте посмотрим два примера в которых будут возвращаться четные числа от 2 до 10. Первый будем реализовывать класс без yield return:

class MyCollection : IEnumerable, IEnumerator
{
    int _current = 0;

    public int Current
    {
        get
        {
            if (_current <= 10)
            {
                return _current;
            }
            throw new IndexOutOfRangeException();
        }
    }

    object IEnumerator.Current => Current;

    public bool MoveNext()
    {
        if (_current < 10)
        {
            _current += 2;
            return true;
        }
        return false;
    }

    public void Reset()
    {
        _current = 0;
    }

    public void Dispose()
    {

    }

    public IEnumerator GetEnumerator()
    {
        return new MyCollection();
    }           

    IEnumerator IEnumerable.GetEnumerator()
    {
        return new MyCollection();
    }
}
Как видно (ну я на это как минимум надеюсь), у нас класс кроме интерфейса IEnumerable реализует еще и интерфейс IEnumerator. В чем разница: 
IEnumerable - говорит: "Я могу предоставить способ последовательного перебора коллекции". И именно в этом интерфейсе объявлены методы GetEnumerator.
IEnumerator - говорит: "Я могу отдать текущий элемент, перейти к следующему, вернуться в начало". К этому интерфейсу принадлежат все остальные методы в примере.
В чем идея, в цикле foreach один раз вызывается метод GetEnumerator, а уже в полученном объекте, до тех пор, пока метод MoveNext не вернет false, берется current и записывается в переменную цикла. В общем виде последовательность такая:
var items = <объект поддерживающий IEnumerable>.GetEnumerator();
пока (items.MoveNext())
{
  var item = items.Current;
  // Здесь полезная работа
}
Надеюсь все понятно. Теперь, как можно переписать этот пример, при помощи синтаксического сахара yield return:
class MyCollection : IEnumerable
{
    public IEnumerator GetEnumerator()
    {
        for (int i = 2; i <= 10; i += 2)
        {
            yield return i;

        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}
Обратите внимание насколько уменьшился код и на то, что нам не пришлось реализовывать второй интерфейс, все это оказалось спрятано внутри синтаксического сахара yield return. При вызове вот такого кода, что для первого, что для второго примера:
static void Main(string[] args)
{
    foreach (var item in new MyCollection())
    {
        Console.WriteLine(item);
    }
    Console.ReadKey();
}
Вы увидите на консоли четные числа от 2 до 10. Как писать, выбирать вам.

2 комментария:

  1. public IEnumerator GetEnumerator()
    {
    yield return 2;
    yield return 4;
    yield return 6;
    yield return 8;
    yield return 10; }

    ОтветитьУдалить
    Ответы
    1. Ага, причем ваш вариант будет более производителен, так как не будет затрат на проверку итератора цикла, его изменение. Но вот программисты, ленивые люди, не любят писать подряд сто строк вместо цикла, не экономят процессорные такты :(

      Удалить