пятница, 21 августа 2009 г.

Генераторы в C# или "бесполезное" yield

Язык о котором пойдёт речь вышел достаточно давно, однако, судя по реальному опыту общения, мало кто пользуется всеми имеющимися возможностями. Этот пост я посвящу ключевому слову yield в языке C# 2.0.
Сначала несколько слов о тенденциях развития С#. Вспоминая каким был C# 1.0, могу сказать, что это была калька с Java2SE, причем не сказать что удачная. Чистый ООП язык, практически слизаный с Java, без какой-либо собственной красоты и лоска, без своего шарма. Однако уже вторая версия очень сильно порадовала своими возможностями, а именно некоторым движением в сторону функционального программирования. Эта же тенденция существенно продолжается и в третьей версии (VS2008), в C# 4.0 (VS2010) функциональные возможности будут расширены ещё больше, при этом Microsoft добавляет в платформу .NET полноценный функциональный язык, который много лет разрабатывался Microsoft Research. По-моему говорит это о многом.
А теперь к делу!
Ключевое слово yield изначально было предназначено для облегчения разработки классических итераторов .NET, а именно классов, реализующих интерфейсы IEnumerator<T> и IEnumerable<T>. То есть, разработка своей коллекции, или способа обращения к существующей коллекции требовала разработки как минимум одного класса - итератора с интерфейсом IEnumerator<T>, по одному классу на каждый способ прохода коллекции. Довольно муторное и однообразное занятие.
И тут к нам на помощь приходит yield!
Небольшой пример. Пусть нам хочется получить все элементы списка находящиеся в диапазоне [3;5]. Вместо стандартного for напишем следующий кусок:

IEnumerator<int> GetIntervalled(List<int> Collection)
{
for(int i=0;i<Collection.Count;i++)
{
if(Collection[i]>=3 && Collection[i]<=5) yield return Collection[i];
}
}

Использовать только что написанный код можно как и обычно используются итераторы – foreach. При очередном выполнении yield return функция вернет управление вызывающей, и передаст соответствующий элемент. На самом деле компилятор по этому коду создаст виртуальный класс реализующий интерфейс IEnumeratot<t>, но нас это не должно интересовать. Подобная конструкция есть первый, маленький шажок в сторону функционального программирования – это один из вариантов продолжения (continuations), потому что визуально код, при повторном вызове, продолжает выполняться с того места где выполнение было окончено. Подобная конструкция в некоторых языках программирования (откуда она и была заимствована) называется генератором, отсюда и название заголовка :)
Конечно, приведенный выше пример абсолютно неинтересен, и ничем не отличается по смыслу от примера, приводимого в документации. Понять, зачем применять столь хитрую конструкцию крайне тяжело. Для того, чтобы стало немножко больше понятно зачем нужны такие вещи приведу пример из реального кода.
В формочку (Windows.Forms) выводятся некоторые сущности, а пользователь отмечает галочками те, которые ему необходимы. Мне хотелось получить отмеченные галочкой сущности, yield пригодился как нельзя кстати:

public IEnumerable<DatabaseImportCase> SelectedImportCases
{
get
{
for (int i = 0; i < lbImportPackages.CheckedIndices.Count; i++)
{
yield return ShownImportCases[i];
}
}
}

ShownImportCases – это список отображаемых сущностей, lbImportPackages – листбокс с галочками (CheckedListBox), в котором отображаются эти сущности. Естественно предполагается, что индексы отображаемых на форме и хранимых в памяти сущностей совпадают. Операции по получению элементов происходят с помощью «ленивых вычислений», то есть получение очередного элемента произойдет только тогда, когда вы его потребуете. Никаких виртуальных списков в памяти не строится. Итак, yield – это универсальный, очень удобный способ получения новых, произвольных по сложности итераторов. Основное его преимущество – уменьшение количества не имеющего смысловой нагрузки кода. То есть, использование этого приема, это просто syntax sugar, но вряд ли кто откажется от сладкого :) Безусловно появление LINQ, а именно LINQ to Objects существенно снизило ценность yield, поскольку практически все итераторы можно получить с помощью запросов LINQ, однако понимание работы такого рода продолжений крайне необходимо для понимания более сложных конструкций функционального программирования. Которые я постараюсь рассмотреть в одном из следующих постов.

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

  1. "это один из вариантов продолжения (continuations), потому что визуально код, при повторном вызове, продолжает выполняться с того места где выполнение было окончено"

    Вот! Спасибо! Именно этих строчек мне и не хватало чтобы до конца понять оператор yield. А то при виде кода наподобие

    public static IEnumerable<int> fib()
    {
    yield return 0;
    int i = 0, j = 1;
    while (true)
    {
    yield return j;
    int temp = i;
    i = j; j = temp + j;
    }

    }
    мозги заклинивало, и я не мог понять, как это работает, там же в первой строчке стоит return(пусть и вместе с yield).

    А теперь всё встало на свои места. Спасибо еще раз!

    ОтветитьУдалить