Секреты LINQ. Автогенерируемые свойства. Инициализаторы объектов и коллекций, страница 10

static IEnumerable<string> GetWords (string[] words)

{

foreach (string w in words)

yield return w;

}

Метод GetWords принимает на входе массив строк, проходит по нему и почленно копирует элементы в перечислимую последовательность IEnumerable<string>. Но он делает это виртуально (не в момент вызова). Главная функция хранит последовательность в объекте query. При необходимости, она может ее использовать, например, вывести. Вы скажете, что для вывода массива не надо было городить такой огород. Учтите, пример создан только для иллюстрации синтаксиса оператора yield.

¨  Поставьте точку останова на строку вызова GetWords, запустите приложение в режиме отладки (F5). После останова нажмите F11 (вход в функцию) и убедитесь, что управление не передается внутрь GetWords. Это произойдет позже, при проходе по результатам запроса query (то есть, выполнении цикла foreach в главной функции). Странно, не правда ли? Есть вызов функции, но он не происходит.

¨  Попробуйте заменить IEnumerable на List — вы получите ошибку.

¨  Попробуйте убрать yield — вы получите ошибку.

Оценивая результаты эксперимента, можно сказать, что вызов GetWords (с итерационным блоком yield) не вызывает функцию, а создает некий код, который будет работать позже, при обращении к результатам запроса (последовательности query). При каждом обращении к этому коду цикл не начинается заново, а продолжает работать, так как он помнит свои предыдущие состояния.

Приведем более сложный пример использования yield. Добавьте следующий статический метод в класс Program вашего консольного приложения. В нем также используется yield. Для того, чтобы понять логику происходящего, придется приложить некоторые усилия.

static IEnumerable<int> Powers (int n)

{

for (int i = 1, d = 0; i < (1<<n); d = i)

yield return i += d;

}  

Попробуйте догадаться, что будет выведено в результате вызова метода Powers с аргументом 10. Для вызова метода из Main используйте следующий фрагмент кода.

Console.WriteLine ("\n\nPowers\n");

foreach (var v in Powers(10))

Console.Write (v + ", ");

Здесь, как видите, мы сразу используем результат, а не запоминаем его в последовательности IEnumerable<int>. Код цикла с yield при каждом новом обращении помнит свое предыдущее состояние. При пошаговом выполнении (F11) этого кода кажется, что функция Powers вызывается многократно, но это не так.

Generic-классы

Обобщенные (универсальные) классы позволяют повысить производительность программного кода. Вы уже знаете, что класс ArrayList позволяет хранить объекты произвольного типа. Но при работе с ними приходится платить за универсальность: приводить типы и/или осуществлять упаковку-распаковку (boxing-unboxing). Эти операции снижают производительность. Проще работать с generic-классом List<type>. При задании типа (параметра type) компилятор генерирует класс, дающий возможность работать с динамическим списком и настроенный на конкретный тип type. Необходимость приводить типы и операции упаковки-распаковки исчезают.

Универсальные (generic) классы повышают повторную используемость двоичного кода. Такой класс может быть определен один раз, а далее, на его основе могут быть созданы объекты многих других типов. Важно, что при этом не нужно иметь доступ к исходному коду, как это необходимо в случае шаблонов C++. В C# в подобных случаях работает механизм рефлексии, основанный на мета-данных. Всю необходимую информацию можно получить из мета-данных, которые добываются из двоичного кода. Так, для создания нового класса List<Person> нет необходимости иметь исходный код шаблона List<T>. Все, что надо для генерации нового класса List<Person> компилятор получит из сборки System.Collections.Generic.

Рассмотрим пример разработки универсального (generic) класса BinaryTree<T>. На его основе компилятор способен генерировать множество классов, умеющих работать с бинарными деревьями поиска.