Секреты linq

63
Черносвитов Александр. Секреты LINQ. © 2007-2010 _______________________________________________________________________________________________________ _____________________ Секреты LINQ Язык С# в отличие от C++ не стоит на месте. Команда Anders Hejlsberg'а (автора языка С#), Microsoft продолжает обновлять и шлифовать его. Если попытаться дать короткое определение LINQ, то оно будет выглядеть примерно так. LINQ — это множество расширений языка C# (и VB), которые поддерживают работу с данными произвольного формата, обеспечивая удобства Intellisense и безопасность типов. Рассмотрим еще одно определение: LINQ — это выразительный синтаксис описания операций над данными, которые невозможно естественным образом смоделировать объектно-ориентированным способом. Технология LINQ (Language Integrated Queries) вплотную подошла к функциональному программированию. Она пользуется функциями (точнее, делегатами — обобщенным понятием указателей на функции) так же, как языки императивного программирования пользуются переменными числовых типов. Такие языки, как LINQ, SQL, XQuery, являются декларативными, они описывают цель запроса. В то же время, языки программирования вида: C++, C#, Java, PL/I, Ruby, Python являются императивными. Они описывают серию шагов, необходимых для достижения цели. На мой взгляд, уместны следующие аналогии. Фундаментальная алгебра определяется как совокупность двух множеств: A = <R, O>, где R — множество объектов, O — множество операций над ними. Например, алгебра вещественных чисел = <R, O>, где R — множество вещественных чисел, O — множество операций с ними: { +, –, / , · }. Класс, используемый в объектно-ориентированном программировании, — это также совокупность двух множеств: C = <D, M>, где D — множество данных класса, M — множество методов, с помощью которых производятся манипуляции с данными. Аналогия между классом в ООП и алгеброй очевидна. Некоторые разделы элементарной математики оперируют множествами чисел (целых, рациональных, вещественных). Некоторые разделы высшей математики оперируют множествами функций (непрерывных, разрывных, от одного или множества вещественных, или комплексных аргументов). Программирование (как структурное, так и объектно-ориентированное) относится к функциональному программированию так же, как элементарная математика — к высшей. Это утверждение менее очевидно, но в нем есть доля правды, так как в функциональном программировании мы оперируем функциями так же, как мы оперируем данными в структурном и ОО-программировании. Работать с кодом, как с данными, является типичным приемом функционального программирования. Лямбда-выражения, которые стали доступными в языке C#, работают с кодом, как с данными. Технология LINQ использует λ-выражения (Lambda Expressions) и деревья выражений (Expression Trees) — понятия, которые появились в функциональном программировании. Деревья выражений пытаются представить код в виде древовидных структур данных. Каждым узлом такого дерева является выражение (Expression), роль которого может выполнять функция, или оператор языка C#. Управляя узлами дерева выражений, можно генерировать λ-выражения, которые широко

Upload: alexold

Post on 22-Oct-2014

1.261 views

Category:

Documents


9 download

TRANSCRIPT

Page 1: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

Секреты LINQ

Язык С# в отличие от C++ не стоит на месте.

Команда Anders Hejlsberg'а (автора языка С#), Microsoft продолжает обновлять и

шлифовать его.

Если попытаться дать короткое определение LINQ, то оно будет выглядеть примерно так. LINQ — это множество расширений языка C# (и VB), которые поддерживают работу с данными произвольного формата, обеспечивая удобства Intellisense и безопасность типов. Рассмотрим еще одно определение: LINQ — это выразительный синтаксис описания операций над данными, которые невозможно естественным образом смоделировать объектно-ориентированным способом.

Технология LINQ (Language Integrated Queries) вплотную подошла к функциональному программированию. Она пользуется функциями (точнее, делегатами — обобщенным понятием указателей на функции) так же, как языки императивного программирования пользуются переменными числовых типов. Такие языки, как LINQ, SQL, XQuery, являются декларативными, они описывают цель запроса. В то же время, языки программирования вида: C++, C#, Java, PL/I, Ruby, Python являются императивными. Они описывают серию шагов, необходимых для достижения цели.

На мой взгляд, уместны следующие аналогии.

Фундаментальная алгебра определяется как совокупность двух множеств: A = <R, O>, где R — множество объектов, O — множество операций над ними. Например, алгебра вещественных чисел = <R, O>, где R — множество вещественных чисел, O — множество операций с ними: { +, –, / , · }.

Класс, используемый в объектно-ориентированном программировании, — это также совокупность двух множеств: C = <D, M>, где D — множество данных класса, M — множество методов, с помощью которых производятся манипуляции с данными.

Аналогия между классом в ООП и алгеброй очевидна.

Некоторые разделы элементарной математики оперируют множествами чисел (целых, рациональных, вещественных). Некоторые разделы высшей математики оперируют множествами функций (непрерывных, разрывных, от одного или множества вещественных, или комплексных аргументов).

Программирование (как структурное, так и объектно-ориентированное) относится к функциональному программированию так же, как элементарная математика — к высшей. Это утверждение менее очевидно, но в нем есть доля правды, так как в функциональном программировании мы оперируем функциями так же, как мы оперируем данными в структурном и ОО-программировании.

Работать с кодом, как с данными, является типичным приемом функционального программирования. Лямбда-выражения, которые стали доступными в языке C#, работают с кодом, как с данными. Технология LINQ использует λ-выражения (Lambda Expressions) и деревья выражений (Expression Trees) — понятия, которые появились в функциональном программировании. Деревья выражений пытаются представить код в виде древовидных структур данных. Каждым узлом такого дерева является выражение (Expression), роль которого может выполнять функция, или оператор языка C#. Управляя узлами дерева выражений, можно генерировать λ-выражения, которые широко используются при построении интегрированных запросов к источникам данных различной природы.

Роль источника данных LINQ может играть: любая перечислимая структура данных (коллекция), Web-service, XML-файл, или реальная база данных. Более точно, источником данных LINQ может быть любой класс, реализующий интерфейс IEnumerable<T>. Объекты таких классов называют последовательностями LINQ. Итак, запросы LINQ можно применить ко всему, что можно перичислить.

Указанный интерфейс IEnumerable<T> заявляет всего лишь один метод: IEnumerator<T> GetEnumerator(), но он поддержан более чем 50-ю методами, которые являются особыми

Page 2: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

новыми конструкциями, называемыми extension methods (расширения). Все расширения уже реализованы классом Enumerable и о них не надо беспокоиться. Приведем краткое описание некоторых из этих методов:

All<T> — определяет, удовлетворяют-ли все члены последовательности какому-либо условию.

Cast<T> — приводит члены последовательности к указанному типу. Concat<T> — склеивает (concatenates) две последовательности. Contains<T> — определяет, содержит-ли последовательность указанный элемент. ElementAt<T> — возвращает элемент по указанному индексу. Except<T> — генерирует разность множеств, образованных двумя

последовательностями. GroupBy<TSource,TKey> — группирует члены последовательности в соответствии с

указанным селектором. Intersect<T> — генерирует пересечение множеств, образованных двумя

последовательностями. Join<TOuter, TInner, TKey, TResult> — сопоставляет (correlates) элементы двух

последовательностей на основе совпадающих ключей. OrderBy<TSource, TKey> — сортирует элементы последовательности. Range(int start, int count) — генерирует последовательность целых чисел в заданном

диапазоне. Select<TSource, TResult> — проецирует каждый элемент в новую форму. ToArray<T> — создает массив из элементов последовательности. ToDictionary<TSource, TKey> — создает словарь на основе ключей, определяемых

селектором. ToList<T> — создает generic-список из элементов последовательности. Where<T> — фильтрует элементы на основе указанного предиката. Union<T> — объединяет элементы двух последовательностей.

Разные поставщики данных управляются с помощью разных языков: T-SQL, PL/SQL и т. д. Теперь разработчики ПО могут создавать LINQ-провайдеры, которые будут транслировать запросы (LINQ queries) в тексты запросов на родном (native) для провайдера языке SQL. Таким образом, LINQ-запросы становятся независимыми от поставщиков данных. Например, часть классов LINQ, называемая LINQ to SQL транслирует запросы LINQ в T-SQL, а другие классы, в том числе разработанные сторонними организациями, транслируют запросы в другие версии SQL.

Существует другой интерфейс — IQueryable<T>, который является производным от IEnu-merable<T>. Он позволяет работать с запросами LINQ в условиях, когда типы данных известны заранее. Параметр <T> этого интерфейса является ковариантным. Это означает, что он может ссылаться как на базовый, так и на производный тип. С помощью IQueryable<T> реализуются так называемые полиморфные запросы. Например, если мы имеем тип Person и два производных от него типа: Student и Professor, то в качестве параметра шаблона T можно задать Person, но фактически работать с множеством объектов Student и Professor.

Расширения языка C#

Технология LINQ опирается на расширения языка C#, которые были введены в C#, версии 3.0. В следующей версии (.NET Framework 3.5) для поддержки LINQ появились новые классы. В Visual Studio 2008 появились новые шаблоны проектов и средства разработки кода, а в языке C# — новые ключевые слова и синтаксические конструкции. Вначале (.Net Framework 1.0) язык C# имел 77 ключевых слов. Вот они.

abstract as base bool break byte case catch

char checked class const continue decimal default delegate

do double else enum event explicit extern false

finally fixed float for foreach goto if implicit

in int interface internal is lock long namespace

new null object operator out override params private

protected public readonly ref return sbyte sealed short

sizeof stackalloc static string struct switch this throw

Page 3: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

true try typeof uint ulong unchecked unsafe ushort

using virtual volatile void while

Для сравнения напомню, что С++ располагает 44 ключевыми словами. Теперь (в .Net Framework 3.5), кроме указанных 77 слов, в языке C# присутствуют 24 контекстных ключевых слова (Contextual Keywords). Вот они.

add ascending by descending equals

from get global group into

join let on orderby partial (type)

partial (method) remove select set value

var where (generic type con-

straint)

where (query clause) yield

Описатель контекстный указывает на то, что смысл слова зависит от контекста. Например, слово where в применении к универсальному (generic) типу задает ограничение на возможные типы параметра шаблона, а в запросе LINQ это же слово задает условие отбора (фильтр отбора) данных.

?Редко используемыйописатель

Описатель типа Nullable, который сопровождается знаком вопроса (например, decimal?), появился в языке C# до появления технологии LINQ, но ранее он использовался довольно редко. В LINQ-запросах Nullable-типы встречаются значительно чаще, так как они позволяют корректно обыграть ситуацию, когда поля данных в таблице БД не заданы. Например.

int? i = null;

Это вполне законное с точки зрения синтаксиса C# объявление (и присвоение). Значение null может быть присвоено переменной любого из примитивных (value) типов, если в объявлении присутствует знак вопроса. Этот знак не является ключевым словом. Скорее, это модификатор типа. Думаю, что вы не встречали также операцию ?? (два следующие подряд знака вопроса). Эта операция используется для проверки на null. Выполните следующий фрагмент и вы поймете, как работает операция ??.

strings = null,test = s ?? "String s == null";

Console.WriteLine (test);

s = "Set of C# Keywords";test = s ?? "String s == null";Console.WriteLine (test);

Список расширений языка C#

Перед тем, как приступить к составлению запросов LINQ, необходимо познакомиться с расширениями языка C#, на которых они основаны. Сначала перечислим их, а затем познакомимся более подробно.

Неявно типизированные переменные (local variable type inference). Теперь можно писать: var s = "Hi"; и компилятор автоматически превратит это в string s = "Hi";

Автоматически реализуемые свойства (auto-implemented properties). В предыдущем курcе мы часто добавляли в классы пары вида (переменная, свойство) таким образом:

decimal salary; public decimal Salary{get { return salary; }set { salary = value; }

}

Теперь вы можете пользоваться сокращенной записью.

public decimal Salary { get; set; }

Ранее такая запись допускалась только при объявлении интерфейса.

Page 4: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

Инициализаторы объектов и коллекций (object initializers). Предположим, что вы объявили класс:

public class Person{public string Name { get; set; }public DateTime Birth { get; set; }public decimal Salary { get; set; }

}

Теперь вы можете создать объект класса Person, используя только его свойства:

Person alex = new Person(){ Name = "Alex", Birth = new DateTime(1994, 2, 26), Salary = 10000m };

Заметьте, в классе Person нет ни одного конструктора. Автоматически сгенерированный конструктор без параметров пользуется автоматически реализуемыми свойствами.

Generic-делегаты, generic-классы и generic-методы. Наиболее точно передает смысл термина generic прилагательное универсальный. Например, generic-класс — это универсальный класс, настраиваемый на определенный тип данных с помощью параметра. Слово шаблон (template), которое используется в языке С++, лишь приближенно соответствует термину generic. Мы вскоре рассмотрим эту технику.

Лямбда-выражения, которые являются результатом эволюционных изменений понятия делегат.

Возможность расширения существующих типов с помощью статических методов статических классов.

Анонимные типы, или проецирование на произвольные структуры. Каркас языка интегрированных запросов .NET (LINQ). Встроенная поддержка ASP.NET AJAX (Asynchronous JavaScript and XML). Эта технология

позволяет при просмотре Web-страницы незаметно для пользователя (асинхронно) обращаться к серверу. Обращение происходит в рамках другого потока, при его завершении страница не перегружается полностью, а частично дополняется или видоизменяется с помощью внедренного кода на языке JavaScript.

Теперь более подробно рассмотрим описанные нововведения.

Анонимные типы

Анонимные типы сигнализируют компилятору о том, чтобы он сам определил точный тип переменной, исходя из выражения, стоящего в правой части, или из контекста выражения. Например:

var s = "This is a string of characters";var a = new[] { 1, 2, 3, 4 };

Типы переменных s и a будут вычислены компилятором. В первом случае типом переменной будет string, во втором — int[]. Обратите внимание на конструкцию new[]. Здесь не указан тип новой памяти, запрашиваемой у системы (в области managed heap). Ранее (в C# 2.0) следовало писать: int[] a = new int[] { 1, 2, 3, 4 };. Рассмотрим еще один пример использования var.

CultureInfo[] cultures = CultureInfo.GetCultures(CultureTypes.AllCultures);foreach (var v in cultures)Console.WriteLine("{0,52} {1}", v.EnglishName, v.IetfLanguageTag);

Здесь тип переменной v CultureInfo. Еще пример:

InstalledFontCollection collection = new InstalledFontCollection();foreach (var font in collection.Families)Console.WriteLine(font.Name);

Здесь тип переменной font FontFamily. Вычислить его самостоятельно не так просто. Обнаружить искомое имя класса в MSDN, ориентируясь лишь на его смысл, значительно сложнее, чем в Google. К сожалению, структура и реализация справочной системы MSDN всегда отставала от ожидаемого уровня.

Автогенерируемые свойства

Довольно часто приходится добавлять в классы открытые свойства, которые имеют традиционный вид:

public string Name

Page 5: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

{get { return name; }set { name = value; }

}

Строго говоря, такие свойства не нужны (достаточно сделать открытой переменную name и не вводить свойство), но на практике вы встретите большое количество таких решений, так как они упрощают привязку к данным. Вы помните, что механизм DataBinding работает только при наличии открытых свойств.

Синтаксически свойства могут быть закрытыми, но это — nonsense. Свойства для того и вводят, чтобы создать иллюзию прямого доступа к переменным.

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

class Stud{public string Name { get; set; }public List<int> Marks { get; set; }

}

При этом отпадает необходимость объявлять скрытые переменные. Раскрывать код аксессоров get и set теперь нет необходимости — это сделает компилятор. Примеры использования технологии LINQ, которые в изобилии вы найдете в MSDN, довольно часто пользуются таким приемом и вы должны о нем знать.

Возникает справедливый вопрос: "А как же в этом случае защитить данные от произвола пользователя?" Ответ: пользуйтесь традиционным способом объявления свойств или частично защищайте внутреннюю переменную с помощью конструкций вида Asymmetric Accessor Accessibility. Вот примеры:

public string Name { get; private set; }public string Name { get; internal set; }

Первое объявление говорит о том, что значение Name можно изменить только в методах класса, например, в конструкторе. Это означает — один раз в жизни объекта, при его создании. Второй способ установки свойства объекта (internal set) говорит о том, что значение Name можно изменить в любом методе любого класса, принадлежащего одной и той же (своей) сборке — Assembly. Попытка изменить свойство в методе класса из другой (чужой) сборки потерпит неудачу.

Инициализаторы объектов и коллекций

Пример нового способа инициализации объекта с помощью default-конструктора был приведен ранее.

Person alex = new Person(){ Name = "Alex", Birth = new DateTime(1994, 2, 26), Salary = 10000m };

Рассмотрим, как выглядит новый инициализатор массива объектов того же типа:

var persons = new[]{ new Person(){ Name = "Bale", Birth = new DateTime(1981, 3, 26), Salary = 10000m },new Person(){ Name = "Dale", Birth = new DateTime(1974, 1, 11), Salary = 15000m },new Person(){ Name = "Gail", Birth = new DateTime(1964, 6, 12), Salary = 20000m }

};

Заметьте, что нет необходимости указывать тип массива. Он вычисляется (infered) автоматически. Сравните приведенный код с тем, который надо было бы писать ранее (до введения расширений в язык C#). Во-первых, в класс Person надо добавить конструктор с параметрами. После этого можно объявить массив либо так:

Person[] persons ={new Person("Alex", new DateTime(1981, 3, 26), 10000m),. . .

}

либо так:

Person persons;persons = new Person[]

Page 6: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

{new Person("Alex", new DateTime(1981, 3, 26), 10000m),. . .

}

После new всегда надо было указывать заранее известный тип. Теперь же проходит вариант вида: new[] {. . .}. Рассмотрим, как выглядит новый способ инициализации generic-коллекции объектов того же типа:

var persons = new List<Person>{new Person(){ Name = "Bale", Birth = new DateTime(1981, 3, 26), Salary = 10000m },. . .

}

Generic-делегаты

Делегатом называется объект делегатного типа. Такой объект может ссылаться на метод (указывать на функцию в пределах какого-то класса). Объявления, начинающиеся словом delegate, декларируют делегатные типы (не делегаты). Они лишь задают сигнатуры методов (правила их использования), которые могут быть вызваны с помощью делегата. Говорят, что они задают сигнатуры возможных заданий делегатов. Сигнатурой метода называются правила его вызова, а именно: тип возвращаемого им значения и список типов его параметров.

В пространстве имен System (в сборке System.Core) появились встроенные делегатные типы: Action и Func, которые соответствует наиболее типичним сигнатурам методов. Рассмотрим тип Action. Он определен так:

public delegate void Action(); // Это определение уже существует в System.Core.dll

Любой метод с сигнатурой void() соответствует типу Action. Такой метод не требует аргументов и ничего не возвращает в точку вызова. Рассмотрим пример использования самого простого делегатного типа Action.

class Program{static void Inform() { Console.WriteLine(Environment.MachineName); }static void Say() { Console.WriteLine("I am an Action delegate"); }static void Main(){Action a = Inform;a();a += Say;a();

}}

Анализ кода вышеприведенного фрагмента

Action является делегатным типом, определенным в System.Core. Переменная a типа Action ссылается на метод Inform. Это возможно потому, что метод

Inform соответствует сигнатуре делегатного типа Action. Переменная a является делегатом типа Action.

Обращение к делегату: a(); эквивалентно прямому вызову метода Inform();. Оператор a += Say; добавляет в список заданий делегата a ссылку на метод Say.

Следующий за этим оператором вызов делегата a(); выполнит два действия: Inform и Say. Это происходит потому, что список заданий делегата (InvocationList) теперь содержит не одну (как было вначале), а две ссылки: на методы Inform и Say. Оба метода должны соответствовать делегатному типу Action (иметь нужную сигнатуру).

Для иллюстрации сказанного вы можете (в рамках консольного проекта) выполнить следующий код:

class TestDelegate{public void Test() { Action a = new Action(Say);a += new Action(Do);Delegate[] dd = a.GetInvocationList();

Console.WriteLine("Testing Delegate's InvocationList\n");

Page 7: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

int i = 0;foreach (var d in dd)Console.WriteLine("{0}. Method = {1}, CallingConvention = {2}, Target = {3}, Returns = {4}",

(++i).ToString(), d.Method.Name, d.Method.CallingConvention, d.Target,d.Method.ReturnType.Name);

}void Say() { Console.WriteLine("Saying"); }void Do() { Console.WriteLine("Doing"); }

}

class Program { static void Main() { new TestDelegate().Test(); } }

Запустив приложение, вы увидите такой результат.

Testing Delegate's InvocationList1. Method = Say, CallingConvention = Standard, HasThis, Target = Test.TestDelegate, Returns = Void2. Method = Do, CallingConvention = Standard, HasThis, Target = Test.TestDelegate, Returns = Void

В пространстве имен System существует делегатный тип Action<T>, который является универсальным (или, родовым — generic). Это означает, что он определяет не одну сигнатуру, а целое семейство (род) сигнатур.

public delegate void Action<T> (T obj); // Это определение тоже существует в System.Core.dll

Объявление Action<T> выглядит почти так же, как и шаблон функций в C++. Только там вместо ключевого слова delegate используется слово template. Доцент В.А. Биллиг предложил переводить термин generic, как универсальный (см. http://www.intuit.ru/department/pl/csharp/).

Можно сказать, что generic-делегаты являются шаблонами ряда сходных сигнатур. Тип Action можно настроить с помощью параметра шаблона T. Например, объявление Action<int> определяет тип указателей на функции с сигнатурой void (int), а объявление Action<Person> настраивает шаблон на сигнатуру void (Person). Рассмотрим пример. Добавьте в класс Program еще одну версию метода Inform.

static void Inform(string name) // Этот метод является будущим заданием делегата{Console.WriteLine(Environment.ExpandEnvironmentVariables(name));

}

Добавьте в Main код вызова метода Inform с помощью generic-делегата Action<string> и выполните код.

Action<string> s = Inform;s("%SystemDrive%");

Шаблон Action<T> делегата s настроен на сигнатуру void (string). Обращение к делегату: s ("%SystemDrive%"); эквивалентно вызову Inform ("%System-

Drive%");.

Следующая версия шаблона Action<> позволяет создавать делегаты, которые способны вызывать функции с двумя параметрами.

public delegate void Action<T1, T2> (T1 arg1, T2 arg2);

Типы параметров настраиваются с помощью параметров шаблона T1 и T2. Рассмотрим пример. Добавьте в класс Program метод SaveImage, сигнатура которого соответствует типу Action<string, byte[]>.

static void SaveImage(string file, byte[] data) { File.WriteAllBytes(file, data); }

Добавьте в Main код вызова метода SaveImage с помощью generic-делегата типа Ac-tion<string, byte[]>.

byte[] img = File.ReadAllBytes(@"C:\Windows\Web\Wallpaper\Landscapes\img11.jpg");Action<string, byte[]> save = SaveImage;save(@"C:\Test.jpg", img); // Не забудьте потом удалить файл C:\Test.jpg

Библиотека System.Core.dll содержит определение еще двух делегатных типов из семейства Action<>. Они соответствуют void-методам с тремя и четырьмя параметрами. Типы параметров, как и ранее, задаются с помощью параметров шаблона. Семейство шаблонов Action<> позволяет создавать делегаты для вызова void-методов, имеющих от 0 до 4 параметров. Этот шаблон охватывает довольно широкий класс методов, которые могут встретиться на практике. Note! В студии 2010 семейство Action<> расширено и позволяет создавать делегаты для вызова void-методов, имеющих от 0 до 16 параметров.

Page 8: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

Семейство generic-делегатов вида Func<>

В System.Core.dll также определено семейство делегатных типов Func<>, которое соответствует произвольным методам, возвращающим значение произвольного типа. Как и в случае с Action<>, существует несколько объявлений этого делегатного типа. Они отличаются количеством входных параметров (от одного, до четырех). Рассмотрим объявление этого делегатного типа для функций с одним параметром.

public delegate TResult Func <T,TResult> (T arg);

Объявление generic-делегата сообщает программисту и компилятору о том, что Func является типом указателей, способных адресовать любую функцию, которая на входе требует переменную типа T и возвращает переменную типа TResult. Как видите, последний (второй) параметр шаблона (TResult) определяет тип возвращаемого функцией значения, а первый — тип входного параметра функции.

При объявлении делегата (указателя) программист задает конкретные типы (T и TResult), после чего компилятор настраивает шаблон на эти типы, то есть, генерирует делегат (код вызова метода). Имея делегатный тип (сигнатуру делегата), можно определить новый делегат (указатель), например так:

Func<double, double> pFunc = Math.Sin; // pFunc указывает на метод, вычисляющий синусConsole.WriteLine (pFunc(1.5)); // Вызов метода с помощью pFunc. Результат: 0,997494986604054pFunc = Math.Sqrt; // Теперь pFunc указывает на метод, вычисляющий квадратный кореньConsole.WriteLine (pFunc(1.5)); // Результат: 1,22474487139159

Указателю pFunc можно присвоить имя (вы помните, что имя — это адрес) функции Sin или функции Sqrt. Указателю можно присвоить имя (адрес) любой другой функции с сигнатурой double (double).

Делегатный тип Func описывает любую функцию с одним параметром типа T, которая возвращает переменную типа TResult. С помощью делегата Func <Person, int> p, настроенного на типы Person и int, можно вызвать несколько разных методов, например: int GetHash (Person), или int GetPersonID (Person).

В пространстве имен System определены еще несколько делегатных типов вида Func<>. Например.

delegate TResult Func <T, U, TResult> (T a, U b);

Этот тип соответствует делегатам с двумя входными параметрами произвольных типов (T, U). Тип возвращаемого значения определяется параметром шаблона TResult. Развивая далее эту концепцию, разработчики (Anders Hejlsberg и Don Box) ввели определение делегатных типов Func<> с тремя и четырьмя параметрами. Note! В студии 2010 семейство Func<> расширено и позволяет создавать делегаты для вызова методов, имеющих от 0 до 16 параметров.

Во всех случаях, последний параметр определяет тип возвращаемого значения. Эти типы покрывают огромное множество вариантов их использования. Если их не хватит, вы всегда можете создать новый делегатный тип MyFunc<> (с произвольным количеством параметров). Например:

public delegate TResult MyFunc<T, U, V, W, X, TResult>(T t, U u, V v, W w, X x);

static double StupidFunc(int t, int u, int v, int w, string x){return t + u + v + w + double.Parse(x);

}

Добавьте в Main код, который обратится к статическому методу StupidFunc с помощью generic-делегата:

MyFunc<int, int, int, int, string, double> pf = StupidFunc;

Console.WriteLine("pf(1, 2, 3, 4, \"10\") = " + pf(1, 2, 3, 4, "10"));

Повторим общее правило. Объявление delegate res Func<a, b, c, d, e, res>(...); создает новый делегатный тип (generic delegate), который описывает любые функции, имеющие 5 входных параметров (a, b, c, d, e) и возвращающие значение типа res (не void — для void-методов используйте шаблон Action<>).

Page 9: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

Итак, мы создали новый шаблон делегатных типов. После этого следует определить множество параметров шаблона, которое дает заказ компилятору на создание нового делегатного типа. Так, выбрав множество <int, int, int, int, string, double>, мы заставляем компилятор сгенерировать делегатный тип, настроенный на указанную сигнатуру. Другое множество типов заставит компилятор создать другой делегатный тип семейства Func<>.

Делегат pf делегатного типа Func<int, int, int, int, string, double> способен вызывать функции с указанной сигнатурой. Так как сигнатура статического метода StupidFunc соответствует делегатному типу, то делегат способен воспринять адрес функции StupidFunc в качестве своего задания (task). Вы помните, что каждый делегат имеет список заданий (InvocationList). Присвоение pf = StupidFunc помещает адрес функции StupidFunc в список заданий делегата.

Было сказано много слов, а суть проста. Тем не менее, надо учиться проговаривать определения, иначе картина делегатной модели останется в тумане или быстро забудется.

Лямбда-выражения

В .Net Framework 3.0 появилась новая синтаксическая конструкция, которая поддержана операцией =>. Операцию => (lambda operator) следует читать: трансформируется в. С помощью этой операции создаются λ-выражения (Lambda Expressions). Этот термин используется в функциональном программировании.

Напомню, что русскому термину операция соответствует английский — operator, а русскому термину оператор соответствует английский — statement. Термин lambda operator следует переводить, как лямбда-операция. Подобные курьезы называют ложными друзьями переводчика.

В математике операции => соответстует прием, называемый подстановка выражения. Cмысл этой операции в языке C# легче понять, если рассмотреть эквивалентные ей, но уже знакомые конструкции языка. Приведем пример, иллюстрирующий шаги развития концепции λ-выражений. Далее на каждом шаге выполняется один и тот же код, но способ его запуска слегка изменяется.

На первом шаге рассмотрим элементарный код.

static float Half (int x) { return x/2.0f; }

static void Main(){Console.WriteLine (Half (3)); // 1,5Console.WriteLine (Half (-3)); // –1,5

}

Пока мы не видим ничего нового, такой подход к вычислению часто повторяющихся выражений хорошо известен. В приведенном варианте он весьма неэффективен, но смысл его понятен — всякий раз, когда надо найти половину целого числа, вызывается внешняя функция (в C# — это метод класса). Реализация неэффективна, так как в метод надо передать параметр (через стек), а затем еще вернуть результат.

Вспоминаю, что в языках PL/I или Pascal для повышения эффективности в таких случаях пользовались вложенными функциями. Они заметно дешевле, так как видят все окружающие их переменные и не требуют передачи параметров. Нечто подобное теперь появилось и в C#.

На втором шаге развития концепции (в C# 2.0) добавили подход на основе анонимного метода (делегата). Он призван упростить рассмотренные манипуляции.

delegate float FuncType (int x); // Заявлен делегатный тип

static void Main(){FuncType Half = delegate(int x) { return x / 2.0f; }; // Анонимный метод делегатного типаConsole.WriteLine (Half (3));

}

Этот код выполняет те же действия, что и предыдущий, вполне традиционный вариант. Как видите, после описателя delegate отсутствует имя метода (поэтому делегат и называется анонимным). Кроме того, тело внешней функции перекочевало внутрь метода

Page 10: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

Main. Такое решение более экономно. Учитывая сказанное ранее о generic-делегатах (см. описание Func<>), перепишем наш фрагмент в более простом виде.

static void Main(){Func<int, float> Half = delegate(int x) { return x / 2.0f; };Console.WriteLine (Half (3));

}

Определение делегатного типа FuncType отсутствует, его заменило описание Func<>, которое уже имеется в пространстве имен System. Для решения задачи достаточно лишь настроить шаблон Func на нужные типы данных (int, float).

Следующий шаг развития концепции привел к созданию Lambda Expressions — подстановочных выражений. Теперь вместо:

delegate(int x) { return x / 2.0f; };

используют подстановочное выражение:

x => x / 2.0f; // λ-выражение

Заголовок и часть тела метода растворились. Мы не видим ни фигурных скобок, ни оператора return. Параметр x остался в урезанном виде. Применим этот трюк и наш фрагмент станет еще короче.

static void Main(){Func<int, float> Half = x => x / 2.0f; // λ-выражениеConsole.WriteLine (Half(3));

}

Эта версия кода дает тот же результат, что и предыдущие две. Итак, λ-выражение — это всего лишь синтаксис, упрощающий объявление анонимных функций.

Лямбда-выражения похожи на стенографию. Стенографист — умершая профессия, которая некогда была достаточно высоко оплачиваема. Стенографист с помощью особых правил и приемов успевал руками записать беглую речь оратора (почти дословно). Выражение x => x / 2.0f — это краткая запись более длинной версии: delegate(int x) { return x / 2.0f; };

Допустимы некоторые промежуточные, гибридные варианты записи λ-выражений. Например:

delegate float MyFunc (int x); // Заявлен делегатный тип

static void Main(){MyFunc Half = x => x / 2.0f; // λ-выражениеConsole.WriteLine(Half(3));

}

Само λ-выражение также допускает разные формы записи. Например, все три приведенные ниже формы λ-выражения эквивалентны.

x => x / 2.0f;(int x) => x / 2.0f;(int x) => { return x / 2.0f; };

Проанализируйте следующие два эквивалентных фрагмента кода, которые иллюстрируют использование типов Action и Func, и вычислите в уме, что они выведут в консольное окно.

int n = 0;Func<int> Go = () => n++;Console.WriteLine(Go() + ", " + Go() + ", " + Go() + ", " + Go());__________________________________________________________________

n = 0;Action Do = () => Console.Write(n+++", ");Do(); Do(); Do(); Do();

Пояснения

Делегатный тип Action позволяет пользоваться функциями, не возвращающими значений в точку вызова. В примере делегату типа Action присвоено имя Do. Метод Do, реализованный

Page 11: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

в виде λ-выражения, производит вывод переменной n, увеличивает ее значение и дополняет вывод запятой с пробелом.

Если при мысленном анализе выражений возникнут трудности, то заставьте студию выполнить этот код и обдумайте результат. Просмотрите справку по шаблонам Func и Action. Приведенные примеры описывают абсурдные ситуации (вместо всей этой суеты можно было просто несколько раз вывести n++). Но они иллюстрируют синтаксические правила создания и использования λ-выражений.

Важно запомнить, что в коде функции, генерируемой компилятором, при реализации λ-выражения, доступны все локальные переменные того метода, где это выражение записано. Это свойство характеризуют термином — захват окружения ().

Приведем еще три примера, иллюстрирующие работу с делегатом print типа Action.

Console.WriteLine("\n\nTest Action<string>");Action<string> print = c => Console.WriteLine(c);print("Hi from Action<string>");___________________________________________________________________________

Console.WriteLine("\n\nEnum strings ");string[] names = { "Microsoft", "creates", "technologies", "very", "quickly" };Array.ForEach<string>(names, print);___________________________________________________________________________

Console.WriteLine("\n\nLooking for code"); string[] codes = {"Penal code", "Traffic code", "Morse code", "Genetic code","Coding rules", "Coding conventions", "Code names", "Code of behaviour", "Code transmission", "Codex book", "Native code","Pseudocode", "Spaghetti code", "Portable code", "Native code","Scan code", "Access code", "Bar code", "Character code", "Country code"

};Array.ForEach<string>(codes.Where(c => c.Contains("code")).ToArray(), print);

Generic-метод ForEach<>, расширяющий класс Array, мы рассморим немного позже. Но вы итак догадались, что это некий аналог оператора foreach, встроенного в язык C#. С помощью generic-метода ForEach и λ-выражения, можно получить форматированый вывод элементов массива. Например:

Console.WriteLine("\n\nFormat double as decimal");var amounts = new double[] { 10.36, 12.0, 134 };Array.ForEach<double>(amounts, c => Console.WriteLine("{0:c}", c));

Рассмотрим более сложный случай.

const string text =@"Many types implement IEnumerable. Some of these types implement public members that are identical to IEnumerable. For instance, if you have a type MyList that implements a Where method (as does IEnumerable<T>),invoking Where on MyList would call MyList’s implementation of Where.By calling the AsEnumerable() method first and then calling Where,you invoke IEnumerable’s Where method instead of MyList’s.";

List<string> words = new List<string>();words.AddRange (text.Split());

Console.WriteLine("\n\nTest Query Where:");var where = words.Where(c => c == "Where");foreach (var s in where)Console.Write(s + ", ");

Здесь все яcно, кроме вызова extension-метода Where, которого не было в предыдущей версии языка C#. Теперь такой метод обслуживает все списки, массивы и другие коллекции. Попытайтесь ответить на вопрос: Какой тип имеет переменная where? Компилятор вычислит его, исходя из контекста выражения:

var where = words.Where (c => c == "Where");

Простым решением будет посмотреть подсказку Intellisense при наведении курсора на метод Where. Она сообщает, что типом возвращаемого значения будет IEnumer-able<string>. Таким образом, это выражение можно заменить на эквивалентное ей, но не использующее описатель var (анонимного типа).

IEnumerable<string> where = words.Where(c => c == "Where");

Page 12: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

Задача. Определить сигнатуру анонимного делегата, генерируемого на основе λ-выражения:

c => c == "Where"

Ответ. Делегат имеет тип: Func<string, bool>, так как параметр c имеет тип string, а выражение c == "Where" имеет тип bool.

При изучении LINQ вам придется работать с еще более сложными типами generic-коллекций, чем рассмотреный нами IEnumerable<T>. Компилятор конструирует такие типы автоматически и в большом количестве, поэтому, введение описателя var следует воспринимать, как средство, увеличивающее продолжительность нашей жизни.

IEnumerable<T> — интерфейс, который реализован почти всеми коллекциями. Более точно: интерфейс реализован классами, поддерживающими эти коллекции. Правила игры, описываемые этим интерфейсом, выполняют все массивы, так как массивы поддержаны abstract-классом Array, который реализует IEnumerable.

Рекурсивное определение: IEnumerable<T> описывает последовательность объектов типа T, а последовательность обозначает все коллекции, работающие по правилам IEnumer-able<T>.

С учетом сказанного оператор:

var where = words.Where(c => c == "Where");

можно переписать в более "пространной" нотации.

Func<string, bool> func = c => c == "Where"; // Создание анонимного делегата с помощью λ-выраженияIEnumerable<string> where = words.Where(func);// Использование делегата (многократный вызов)

Метод Where является примером применения новой синтаксической конструкции языка C#, называемой extension method (расширяющий метод). Мы рассмотрим расширяющие методы ниже.

Необходимость использования анонимных типов

В рассмотренных примерах мы показали, как вычислить точный тип var-объекта и заменить анонимный описатель var его точным значением. Но бывают случаи, когда сделать это невозможно. Покажем, как создать анонимный объект, тип которого вычисляется компилятором, и отсутствует возможность замены его каким-то конкретным описанием.

var song = new { Artist = "Ray Charles", Song = "Alexander's Ragtime Band" };

Здесь объявлен анонимный тип, который содержит два именованных (не анонимных) свойства Artist и Song. Имя же самого (вновь созданного компилятором) типа недоступно на стадии разработки кода. Так как тип объекта song нам не известен, то описатель var нельзя заменить чем-либо другим.

Отладчик сообщит нам, что тип объекта song — это <Anonymous Type>, но этот описатель не соответствует какому-либо типу библиотеки .NET, он передает лишь семантику использования объекта. Заменить var на <Anonymous Type> не удастся — компилятор его не примет.

Отметьте, что значения свойств Artist и Song можно только читать (изменять нельзя). Интересно то, что, не зная типа объектов, мы можем создать типизированный массив, или любую другую коллекцию этих объектов и спокойно работать с нею.

var songs = new[] { new { Artist = "Low Rowles", Song = "Cheek to cheek" },song,new { Artist = "Frank Sinatra", Song = "September In the Rain" }

};

Аксиома. Все массивы, кроме object[], являются типизироваными.

С помощью следующего кода убедитесь в том, что метод ToString() отлично обслуживает анонимные объекты.

foreach (var item in songs)Console.WriteLine(item);

Page 13: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

Обязательно выполните этот фрагмент в рамках проекта студии и проанализируйте его вывод.

{ Artist = Low Rowles, Song = Cheek to cheek }{ Artist = Ray Charles, Song = Alexander's Ragtime Band }{ Artist = Frank Sinatra, Song = September In the Rain }

Анонимные типы предоставляют удобный способ инкапсуляции множества свойств в рамках анонимного объекта. Формат вывода анонимных объектов очень похож на формат JSON (Java Scrip Object Notation), который часто используется при передаче даных по каналам RSS. Аббревиатура RSS (Really Simple Syndication) обозначает формат XML-документов, используемый Web-сайтами для публикации часто обновляемых данных, таких как: заголовки новостей (news headlines), или ленты посланий (blog posts).

Оператор yield

Контекстное ключевое слово yield [jild] действует как оператор, создающий итерационный блок, который позволяет вычислять выражение и сохранять его в перечислимом объекте. Последний можно использовать по мере надобности (сразу или потом). Рассмотрим код.

static void Main(){string[] words = { "class", "const", "continue", "decimal" };IEnumerable<string> query = GetWords (words);

foreach (string w in query)Console.WriteLine (w);

}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 + ", ");

Page 14: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

Здесь, как видите, мы сразу используем результат, а не запоминаем его в последовательности 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>. На его основе компилятор способен генерировать множество классов, умеющих работать с бинарными деревьями поиска.

Параметр T шаблона BinaryTree<T> определяет тип данных, которые будут хранится в узлах дерева.

Вложенный generic-класс Node<T> реализует функциональность узла дерева. Каждый объект Node<T> хранит ссылку на данные, соответствующие текущему узлу (тип данных определяется параметром T), и две ссылки на (левый и правый) узлы, прикрепленные к текущему узлу.

Единственным полем данных класса BinaryTree<T> является ссылка на корневой узел дерева root.

public class BinaryTree<T> where T : IComparable<T>{public class Node<T> where T : IComparable<T>{public T data;public Node<T> left, right;public Node(T data) { this.data = data; }

}

Node<T> root;public IEnumerable<T> OrderedSet { get { return GetAll(root); } }

void Add(T item, ref Node<T> node){if (node == null)node = new Node<T>(item);

else if (item.CompareTo(node.data) < 0)Add(item, ref node.left);

elseAdd(item, ref node.right);

}

IEnumerable<T> GetAll(Node<T> node){if (node.left != null){foreach (T item in GetAll(node.left))

yield return item;}yield return node.data;if (node.right != null)

Page 15: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

{foreach (T item in GetAll(node.right))

yield return item;}

}

void Print(Node<T> item, int depth, int offset){if (item == null)return;

Console.CursorLeft = offset;Console.CursorTop = depth;Console.Write(item.data);if (item.left != null)Print("/", depth + 1, offset - 1);

Print(item.left, depth + 2, offset - 3);if (item.right != null)Print("\\", depth + 1, offset + 1);

Print(item.right, depth + 2, offset + 3);}

void Print(string s, int depth, int offset){Console.CursorLeft = offset;Console.CursorTop = depth;Console.Write(s);

}

public void Add(T item) { Add(item, ref root); }

public void AddRange(params T[] items){foreach (var item in items)Add(item);

}public void Print() { Print (root, 0, Console.WindowWidth / 2); }

}

Ограничитель where T : IComparable<T> говорит компилятору о том, что не любой тип данных может быть задан в качестве параметра шаблона BinaryTree<T>, а только тип, выполняющий интерфейс IComparable.

Вы знаете, что бинарное дерево поиска должно быть упорядочено по ключу и эти ключи при формировании кроны дерева необходимо сравнивать. Поэтому требование уметь сравнивать объекты, которые надо хранить в дереве, совершенно необходимо для типа, который подается на вход шаблона BinaryTree<T>. Говорят, что шаблон настраивается на определенный тип T.

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

static void TestBinaryTree(){Console.Clear ();string line = new string('\u2500', 22);Console.WriteLine(line + "Test BinaryTree<int>\nPress any key.");BinaryTree<int> tree = new BinaryTree<int>();tree.AddRange(16, 8, 24, 4, 12, 20, 28, 2, 6, 10, 14, 18, 22, 26, 30, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31);

tree.Print();

Console.WriteLine("\n\nNot formatted BinaryTree<int>\n");foreach (var item in tree.OrderedSet)Console.Write(item + ", ");

}

Рекурсивный метод Print показывает, как управлять позициями данных, выводимых в консольное окно. С его помощью мы показывает структуру дерева.

Метод GetAll с помощью оператора yield создает итеративный блок и накапливает информацию об узлах дерева в generic-коллекции IEnumerable<T>.

С помощью свойства OrderedSet универсального класса BinaryTree<T> мы получаем эту коллекцию и выводим ее содержимое простым перечислением, без позиционирования на экране узлов дерева.

Page 16: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

В классе BinaryTree<T> нет средств индексирования элементов, необходимых для реального дерева поиска, в нем также нет методов поиска узлов, нет методов записи данных в файл и чтения из него. Пример служит для иллюстрирации принципов разработки универсальных классов. Рассмотрим, например, как добавить в этот класс метод поиска заданного элемента и отображения пути к нему, начиная от корня.

Так как в этом методе используется рекурсия, то в нем нельзя применить прием с итеративным блоком yield. Документация говорит, что yield нельзя использовать в рекурсивных функциях. Путь к искомому узлу будемхранить в generic-коллекции List<T>.

Добавьте в класс BinaryTree<T> следующее объявление: List<T> path; Добавьте конструктор с кодом создания списка.

public BinaryTree (){path = new List<T> ();

}

Добавьте открытый метод, который пользуется услугами скрытого метода поиска. Такой прием позволяет скрыть детали реализации класса. Пользователь класса не должен помнить корневой узел, с которого начинается поиск узла с определенным значением.

public List<T> FindPath (T item){path.Clear ();FindPath (item, root);return path;

}

Добавьте скрытую версию рекурсивного метода FindPath для поиска узла и формирования списка List<T>, который будет содержать путь к узлу дерева item. При повторных вызовах путь необходимо обнулять и формировать заново. В процедуре поиска, также как и в методе Add, мы используем свойство сравниваемости элементов (см. обращение к CompareTo).

void FindPath (T item, Node<T> node){if (node != null){path.Add (node.data);if (item.CompareTo (node.data) < 0)

FindPath (item, node.left);else

FindPath (item, node.right);}

}

Добавьте в метод TestBinaryTree код вызова FindPath, запустите приложение и проанализируйте результат.

Console.WriteLine ("\n\nTesting FindPath(21)\n");foreach (var item in tree.FindPath (21))Console.Write (item + "->");

Расширение функциональности существующих типов (extension methods)

Как было указано выше, в языке C# появились новые синтаксические конструкции. Рассмотрим одну из них. Она позволяет расширить функциональность существующего типа, даже если он имеет описатель sealed, каким, например, является класс String. Подобный прием давно существует в языке JScript. Теперь, благодаря усилиям команды An-ders Hejlsberg'а (создателя языка C#), такая возможность появилась и в C#.

Представим, что нас не устраивает функциональность класса Object и мы хотим ее расширить, но не посредством создания производного класса (public class MyObject : Object {}), а путем добавления нового метода в уже существующий класс Object. В версии .NET Framework 1.0 такая мысль была бы крамольной. Теперь это можно сделать, добавив пару строк кода. Приведем формальное определение extension-метода.

Page 17: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

Extension method: A static method that can be invoked by using instance method syntax. In effect, extension methods make it possible to extend existing (and constructed) types with addi-tional methods. Расширяющий метод — это статический метод, синтаксис вызова которого совпадает с синтаксисом вызова обычного метода.

Напомню, что обычные нестатические методы называют instance methods, их вызов производится для объекта, а не класса, например: obj.Method(). Статические методы называют class methods, их вызов происходит для класса, а не для объекта, например: Class.Method(). Все обычные методы при вызове неявным образом получают ссылку this, которая указывает на тот объект, для которого работает метод. Наоборот, статические методы не имеют такой ссылки, они работают со статическими переменными (общими данными для всех объектов класса). Статические методы — это аналоги глобальных функций в структурном программировании.

Расширяющий метод — это статический метод статического класса. Но теперь он имеет скрытый this-параметр, который определяет не объект (как скрытый this-параметр в обычном методе), а тип, который необходимо расширить. Начиная с момента объявления extension-метода, вы можете работать с ним так, как будто он изначально присутствовал в том типе, который расширен. Здесь важно то, что вызов extension method'а будет выглядеть вполне естественно — так, как если бы он изначально присутствовал в расширяемом классе, то есть, был обычным нестатическим методом.

Для создания статического необычного (или extension-) метода следует поместить его в статический класс, и особым способом определить первый параметр. Имя статического класса с расширяющим методом произвольно, а имя метода задает расширение — новую, добавляемую функциональность.

Вам хочется расширить класс String так, чтобы он убирал лишние пробелы не только в начале и конце слова, но и между словами? Теперь вы можете это сделать, добавив в какой-либо статический класс статический метод с такой сигнатурой.

public static string TrimAll(this string s);

Первый, скрытый от пользователя, параметр (this string s) говорит компилятору, что мы собираемся расширить функциональность класса String. Рассмотрим пример, который добавляет указанный extension-метод в класс String. Добавьте в любой консолный проект новый статический класс с произвольным именем. Например.

public static class Extender // Имя класса произвольно{public static string TrimAll(this string s) // Главную роль в списке параметров метода играет описатель this{return new Regex(@"\s{2,}").Replace(s.Trim(), " ");

}}

Проверить результат расширения функциональности класса можно в рамках любого другого класса (например, в стандартном классе консольного проекта — Program). Важно лишь обеспечить видимость класса Extender. Для этого поместите класс Extender в то же пространство имен, в котором живет класс Program. Например.

static void TestTrimAll(){var text = " This text has many extra spaces ";Console.WriteLine(text.TrimAll());

}static void Main(){Console.BackgroundColor = ConsoleColor.Blue;Console.ForegroundColor = ConsoleColor.Yellow;Console.Clear();

TestTrimAll();}

Запустите проект на выполнение и выпоните его пошагам (F11). Убедитесь, что результат соответствует тому, что ожидалось. Новый, необычный контекст использования ключевого слова this используется в .NET Framework 3.5 для включения механизма extension. Мы могли бы добавить и второй параметр. Он был бы открыт для пользователя. Например, параметр вида char c — символ (не обязательно пробел), который надо подстричь.

Page 18: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

Рассмотрим пример, который расширяет класс Object и добавляет в него extension-метод IsIn, возвращающий результат поиска произвольного объекта в произвольной коллекции. Этот пример часто используют для иллюстрации логики cоздания расширений. Добавьте в класс Extender новый метод.

// Второй параметр (коллекция col) играет вспомогательную рольpublic static bool IsIn (this object o, IEnumerable col){foreach (var v in col){if (v.Equals(o))

return true;}return false;

}

Обратите внимание на параметры. Описатель this явно указывает, что метод IsIn является расширением. Далее следует тип, который следует расширить. В нашем случае им является object. Второй параметр определяет коллекцию, в которой следует поискать объект o.

Добавьте в класс Program определение вспомогательной статичекой переменной

static string line = "\n" + new string('\u2500', 60) + "\n";

Добавьте в класс Program метод TestIsIn, который проверяет результат расширения функциональности класса Object, а внутрь Main добавьте код вызова этого метода.

static void TestIsIn(){Console.WriteLine(line + "Test IsIn");

var teams = new string[] { "Spartak", "Zenith", "Locomotive" };bool b = "Zenith".IsIn(teams);Console.WriteLine("Zenith is in teams array: " + b);

var ints = new[] { 1, 2, 3, 4 };Console.WriteLine("3 is in integer array: " + 3.IsIn(ints));

var date = new DateTime(2009, 1, 2);var date2 = DateTime.Now.AddMilliseconds(-1);var dates = new[] { new DateTime(2009, 1, 3), new DateTime(2005, 2, 6),date, DateTime.Now, date2

};

Console.WriteLine("date {0:d} is dates: {1}", date, date.IsIn(dates));Console.WriteLine("date {0:d} is in dates: {1}", DateTime.Now, DateTime.Now.IsIn(dates));Console.WriteLine("date {0:d} is in dates: {1}", date2, date2.IsIn(dates));

}

Класс Extender содержит метод, расширяющий тип object, а, следовательно и все другие типы .NET Framework, так как все они происходят от object. Механизм расширений позволяет расширить любой другой (более специализированный, чем object) тип данных. Мы выполнили это в первом примере.

Попробуйте мысленно проиграть код и вычислить производимый им вывод. Объясните, почему результат вызова DateTime.Now.IsIn(dates) зависит от того, как он выполняется (по шагам, или нет). В тело метода Main() вручную введите 3. (точка) и оцените список доступных методов в подсказке Intellisense. Что выведет следующий фрагмент?

var ints2D = new int[][] {ints,new[] { 2, 3, 4 },new[] { 1, 2, 3, 4, 5, 6, 7 }

};Console.WriteLine("ints {0:d} is in ints2D: {1}", ints, ints.IsIn(ints2D));

Проанализируйте синтаксис создания массива массивов и алгоритм поиска массива в массиве массивов.

Рассмотрим код статического класса, который расширяет все типы, реализующие интерфейс IList, а также тип object и string.

public static class Dumper{

Page 19: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

// ======= Первый аргумент extension-метода должен иметь описатель thispublic static void Dump(this IList list) // Расширяем IList{foreach (object o in list)o.Dump();

}

public static void Dump(this object o){PropertyInfo[] properties = o.GetType().GetProperties();foreach (PropertyInfo p in properties){try{

Console.WriteLine(string.Format("{0} --> {1}", p.Name, p.GetValue(o, null)));}catch{

Console.WriteLine(string.Format("{0} --> {1}", p.Name, "unknown"));}

}}

public static void Show(this string what) { Console.WriteLine(what); }}

Попытайтесь самостоятельно создать код для проверки этих методов. Применим созданное расширение для одиночного объекта класса string:

Console.WriteLine("\nTest Extension Method");string s = "Now all the strings are extended with Dump and Show";s.Dump();s.Show();

В extension-методе Dump(this object o) мы опрашиваем все свойства произвольного объекта. Класс string имеет всего два свойства: индексатор Chars, трактуемый как свойство (prop-erty), и обычное свойство Length. Поэтому мы увидим следующий результат.

Chars --> unknownLength --> 51

Заметьте, что любой индексатор трактуется .NET Framework, как индексируемое свойство.

В нашем случае индексатор не выводит значения, так как индекс не задан. Теперь применим расширение Dump к анонимному объекту song.

Console.WriteLine("\nDump object:");var song = new { Artist = "Ray Charles", Song = "Alexander's Ragtime Band" };song.Dump();

Вывод, производимый Dump, очевиден.

Artist --> Ray CharlesSong --> Alexander's Ragtime Band

Можно-ли применить метод Show для объекта song?

Вторая версия метода Dump(this IList) применима только к коллекциям объектов. Проверим ее с помощью массива анонимных объектов и заодно посмотрим, как работает расширение IsIn для объекта song.

Console.WriteLine("\nDump array of objects:");var songs = new[] { new { Artist = "Low Rowles", Song = "Cheek to cheek" },song,new { Artist = "Frank Sinatra", Song = "September In the Rain" }

};songs.Dump();Console.WriteLine ("song.IsIn(songs): " + song.IsIn(songs));

Добавьте в класс Extender следующий метод и убедитесь, что класс String пополнился методом LengthOrNull.

public static int? LengthOrNull (this string s) { if (s == null) return null; else return s.Length; }

Для проверки этого факта можно выполнить такой фрагмент.

string s = null;

Page 20: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

int? len = s.LengthNull();Console.WriteLine("String: '{0}' has length: {1}", s, len ?? 0);s = "LengthNull extends class String";len = s.LengthNull();Console.WriteLine("String: '{0}' has length: {1}", s, len ?? 0);

Поясните логику использования модификатора ? и операции ??. Забегая вперед, покажем, как работает интегрированный запрос LINQ. Добавьте в Main такой фрагмент кода.

ArrayList tList = new ArrayList(teams);var q = from s in teams select s;Console.WriteLine("\n\nTeams List has {0} items", q.Count<string>());foreach (string s in q)Console.Write(s + ", ");

Extension-методы класса Enumerable

В предыдущем параграфе мы показали, как добавить метод, расширяющий возможности какого-либо типа. В классах пространства имен System.Linq существует множество таких методов (extension methods). Их используют при работе с последовательностями произвольных типов. Напомним, что последовательностью называется любая коллекция, реализующая интерфейс IEnumerable<T>.

Заметим, что все типы коллекций, с которыми вы встречались до сих пор: Array, ArrayList, Stack, Queue, Hashtable, SortedList реализуют интерфейс IEnumerable. Все generic-коллекции: Stack<T>, Queue<T>, List<T>, LinkedList<T>, Dictionary <TKey, TValue>, а также базовый класс Collection<T>, служащий для создания ваших собственных generic-коллекций, реализуют интерфейс IEnumerable<T>. Все указанные коллекции подходят под определение последовательности.

Большинство запросов LINQ реализуется с помощью статических extension-методов, которые определены в статическом классе System.Linq.Enumerable. В качестве первого (this) параметра все эти методы имеют переменную типа IEnummerable<T>, что призвано расширить функциональность последовательностей объектов произвольных типов.

Анонимные объекты, или проецирование на произвольные структуры

Анонимные типы — это сокращенная форма инициализатора объекта, позволяющая пропустить описание типа. Рассмотрим, например, инициализатор массива customers.

var customers = new[]{new { FName="Arthur", LName="Conan Doyle", Phone="323-3232", City="Edinburgh", Addr="Stonyhurst 32" },new { FName="Sherlock", LName="Holmes", Phone="555-5555", City="London", Addr="Baker st 221B" },new { FName="John", LName="Watson", Phone="555-5555", City="London", Addr="Baker st 221B" }

};

Составим запрос к массиву customers.

var query = customers.Select(c => new { c.LName, c.Addr });

Результат запроса может быть выведен в консольное окно таким образом.

foreach (var item in query)Console.WriteLine(item);

Обратите внимание на часть строки LINQ-запроса:

. . . new { c.LName, c.Addr };

В ней пропущено описание типа. Этим типом мог быть класс Customer, если бы такой класс был объявлен ранее. Но его нет. Так как extension-метод Select (или запрос) требует выбрать только два поля из пяти, указанных в инициализаторе объекта, то говорят, что объекты проецируются на кортеж (c.LastName, c.Addr).

Термин кортеж принят в нашей литературе по дискретной математике. В англоязычной литературе используют термин tuple, который имеет тот же смысл: упорядоченная последовательность полей. Иногда говорят n-tuple. В некоторых наших изданиях используют термин "n-ка" (энка — последовательность из n элементов).

При обработке запроса компилятор создает объекты типа <Anonymous Type>. Вы можете увидеть этот тип в окне отладчика Autos при пошаговом выполнении кода. Найдите в этом

Page 21: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

окне, какой тип компилятор вывел (infered) для самого запроса (объекта query). Описание этого типа выглядит так.

Enumerable.WhereSelectArrayIterator<<>f__AnonymousType1<string,string,string,string,string>,<>f__AnonymousType2<string,string>> . . .

Теперь вам должно быть понятно, для чего в язык C# добавили описатель var. Этот короткий описатель способен заменить любой из временно создаваемых компилятором типов. Временные типы, как видите, могут иметь ужасающий вид.

Предикаты

В пространстве имен System описан generic-делегат (то есть, делегатный тип) Predicate<>. Он определяет сигнатуру bool Predicate<T>(T). Переменная этого типа способна указывать на любую функцию, которая требует параметр произволного типа T и возвращает true или false. Покажем, как использовать предикат для вызова extension-метода FindAll. Последний работает с входной последовательностью и возвращает выходную последовательность, каждый элемент которой удовлетворяет условию, заданному предикатом.

Console.WriteLine("\nTest FindAll for Array");Action<int> print = c => Console.Write(c + ", ");

int[] fib = { 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 }; // Ряд чисел FibonacciPredicate<int> pred = c => c % 2 == 1;Array.ForEach<int> (Array.FindAll<int>(fib, pred), print);

Работа ведется с массивом целых чисел. Generic-делегат print на лету создает функцию, которая выводит в консольное окно

свой параметр. Generic-делегат pred создает функцию, возвращающую результат проверки своего

аргумента на нечетность. Extension generic-метод FindAll пробегает по последовательности, заданной первым

аргументом (fib), и применяет к каждому ее элементу метод, заданный делегатом print.

Extension generic-метод ForEach пробегает по последовательности, заданной первым аргументом, и применяет к каждому ее элементу метод, заданный делегатом pred.

Необходимо научиться правильно называть новые реалии и операции, производимые с ними. Для практики повторим логику происходящего другими словами.

Предикат pred, имеющий тип Predicate<int> (он же является делегатом), задан λ-выражением. Он позволяет отобрать нечетные числа из последовательности Fibonacci, возвращаемой extension-методом FindAll.

Делегат print типа Action<int> также задан задан λ-выражением, которое воздействует на каждый элемент последовательности, возвращаемой extension-методом ForEach.

Следующий пример производит те же действия, но с generic-списком типа List<int>. Обратите внимание на то, что синтаксис работы с типизированным списком проще.

Console.WriteLine("\nTest FindAll for List<int>");List<int> list = new List<int>(fib);list.FindAll(predicate).ForEach(print);

Категории стандартных операторов запроса

Класс Enumerable имеет множество статических методов для выполнения запросов к последовательностям (коллекцям, реализующим интерфейс IEnumerable<T>). В то же время класс Queryable имеет множество статических методов для выполнения запросов к структурам данных, реализующим интерфейс IQueryable <T>. Эти методы (операции) разбиты на 14 категорий (групп). Следующая таблица отображает эту классификацию.

N Группа операций Операции

1 Фильтрация (Filtering) Where, OfType

2 Проецирование (Projection) Select, SelectMany

3 Разбиение (Partitioning) Take, TakeWhile, Skip, SkipWhile

4 Объединение (Join) Join, GroupJoin

Page 22: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

5 Слияние (Concatenation) Concat

6 Сортировка (Ordering) OrderBy, OrderByDescending, ThenBy, ThenByDescending, Reverse

7 Группировка (Grouping) GroupBy, ToLookup

8 Операции с множествами

(Set)

Distinct, Union, Intersect, Except

9 Преобразование

(Conversion)

AsEnumerable, AsQueryable, ToArray, ToList, ToDictionary, Cast,

OfType, ToLookup

10 Проверка равенства

(Equality)

SequenceEqual

11 Операции с элементами ElementAt, ElementAtOrDefault, First, FirstOrDefault, Last, LastOrDe-

fault, Single, SingleOrDefault,

12 Генерирование

(Generation)

DefaultIfEmpty, Empty, Range, Repeat

13 Кванторы (Quantifiers) Any, All, Contains

14 Агрегирование (Aggregate) Count, LongCount, Sum, Min, Max, Average, Aggregate

Фильтрация данных

Рассмотрим пример использования extension-метода Where, используемого для фильтрации элементов произвольной последовательности. Этот метод расширяет все последовательности IEnumerable<T> и имеет две совмещенные версии. Первая версия имеет прототип:

public static IEnumerable<T> Where<T>(this IEnumerable<T> src, Func<T,bool> pred);

Переменная pred является generic-делегатом типа Func <T, bool> и задает функцию-предикат. Напомним, что предикатом называется любая функция, которая возвращает значение типа bool.

Метод Where пробегает по входной последовательности src и для каждого ее элемента вызывает функцию-предикат. Если результатом вызова предиката будет true, то элемент помещается в выходную последовательность, если — false, то нет. Можно сказать, что предикат является подобием пропуска (фильтра) для элемента входной последовательности. Он решает, можно ли пропустить элемент в выходную последовательность. Приведем пример фильтрации последовательности текстовых строк.

string line = new string('\u2500', 22);Console.WriteLine(line + "Test Where");string[] words = { "cool", "google", "bool", "true", "soon", "do", "bootable", "zoom", "bottom" };var q = words.Where(c => c.Contains("oo"));

Console.WriteLine("Words containing 'oo':\n" + line);foreach (var s in q)Console.WriteLine(s + "\t" + s.Length);

Результат вызова метода Where — это объект, реализующий интерфейс IEnumerable<string>. Этот объект способен выполнить проход по последовательности и отфильтровать данные. Ссылка на него попадает в переменную q (с семантикой query). Лямбда-выражение c=>c.Contains("oo") задает и параметр и тело функции-предиката. Переменная с — это параметр, а выражение c.Contains("oo") — тело предиката. Тело функции должно соответствовать делегату с сигнатурой Func<T,bool>. Так как метод Con-tains возвращает bool, то это условие в нашем случае выполнено. Строки массива фильтруются с помощью λ-выражения и на выходе остаются только слова, содержащие подстроку "oo".

Вторая версия extension-метода Where имеет более сложную сигнатуру:

public static IEnumerable<T> Where<T>(this IEnumerable<T> src, Func<T,int,bool> pred);

Параметр pred типа Func<T, int, bool> является ссылкой на делегатный метод, который на сей раз должен иметь два аргумента: первый, как и ранее, настраивает generic-делегат на пользовательский тип, второй передает целочисленный индекс текущего элемента входной последовательности. Тип возвращаемого значения определен, как bool (все предикаты должны возвращать bool). Индекс текущего элемента можно использовать для фильтрации данных. Следующий пример показывает, как это сделать.

string[] digitNames = { "zero","one","two","three","four","five","six","seven","eight","nine" };

Page 23: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

var q = digitNames.Where((s, i) => s.Length <= i);Console.WriteLine("Name: Length: Pos:\n{0}", new string('\u2500', 21));int k = 0;foreach (var s in q)Console.WriteLine("{0,-8} {1,-5} {2}", s, s.Length, ++k);

Делегатный тип вычисляется на основе анализа λ-выражения: (s, i) => s.Length <= i. При анализе левой части λ-выражения (s, i) компилятор видит, что количество входных параметров — два, поэтому он выбирает версию метода Where, в которой второй параметр i означает индекс текущего элемента в массиве digitNames. Индекс имеет тип int. Поэтому все λ-выражение соответствует делегатному типу Func<T,int,bool>.

Тип параметра T определяется типом элементов входной последоватетьности, в нашем случае — string.

Правая часть выражения (s.Length <= i) имеет тип bool — она выполняет роль предиката.

Подводя итог, заключаем, что все выражение фильтрует последовательность символьных строк и на выход попадают только те строки, длина которых меньше или равна их порядковому номеру. Алгоритм полностью определяется на основе анализа λ-выражения (анонимного делегата). Рассматриваемый фрагмент выводит в консольное окно такой результат.

Name: Length: Pos:____________________four 4 1five 4 2six 3 3seven 5 4eight 5 5nine 4 6

Задание. С помощью отладчика студии определите тип переменной q. Ответ. Компилятор пользуется типом Enumerable.WhereIterator<string>, однако он отражает внутреннюю кухню. Мы не можем использовать этот тип в качестве альтернативы описателю var.

Отложенное выполнение

В двух предыдущих примерах было показано, как работает метод Where класса Enumerable. Заметим, что выполнение метода, а именно: проход по последовательности и отбор значений, удовлетворяющих критерию фильтрации, откладывается до тех пор, когда эти значения не начнут перебираться (be enumerated). В нашем случае это происходит при выполнении цикла foreach. Видимый вызов метода:

var q = digitNames.Where((s, i) => s.Length <= i);

не производит никаких вычислений. Таким же (отложенным) способом выполняется множество других методов из категории extension. Описанный способ вычислений иногда называют ленивым (lazy evaluation). Он реализуется, как вы, вероятно, догадались, с помощью итератора yield return. Такой подход к реализации extension-методов повышает эффективность цепочки вычислений, за счет экономии памяти, отводимой на хранение промежуточных результатов. Рассмотрм пример, в котором используется цепочка вызовов extension-методов.

var songs = new[]{ new { Artist="Low Rowles", Date=new DateTime(1965,3,28), Song="Cheek to cheek" },new { Artist="Ray Charles", Date=new DateTime(1952,6,11), Song="Alexander's Ragtime Band" },new { Artist="Frank Sinatra", Date=new DateTime(1958,6,7), Song="Emily" },new { Artist="Frank Sinatra", Date=new DateTime(1956,3,28), Song="September In the Rain" },new { Artist="Frank Sinatra", Date=new DateTime(1957,4,12), Song="It started all over again" },new { Artist="Nat King Cole", Date=new DateTime(1956,11,29), Song="Impossible" },new { Artist="Dinah Washington", Date=new DateTime(1975,9,21), Song="Call Me Irresponsible" },new { Artist="Peggy Lee", Date=new DateTime(1978,5,9), Song="As Time Goes By" }

};

var q = songs.Select(s => new { len = s.Song.Length, s.Song, s.Artist, s.Date }).Where(s => s.Date > DateTime.Now.AddYears(-60) ).OrderBy(s => s.len).Select(s => new { s.Song, Date = s.Date, s.len });

int k = 0;

Page 24: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

foreach (var s in q)Console.WriteLine("{0}. {1}, {2,-2}, {3}", ++k, s.Date.ToShortDateString(), s.len, s.Song);

Отметим, что компилятор распознает контекстное ключевое слово yield только если после него следует оператор return или break. Конструкция yield return, помещенная в итерационный блок, заставляет перечислимый класс работать в рамках следующего в цепочке перечислимого класса. Распространяясь далее по всей цепочке, это правило осуществляет задержку выполнения всей последовательности вызовов расширяющих методов.

Немедленное выполнение (Eager Evaluation)

Если изменить первый из двух запросов, рассмотренных ранее (в разделе "Фильтрация данных"), и добавить в него вызов метода ToArray()

string[] oo = words.Where(c => c.Contains("oo")).ToArray();

то метод Where будет выполнен сразу, без задержки. То же самое произойдет с любым другим отложенным запросом. Методы ToArray() и ToArray<T>() также являются расширениями IEnumerable<T>. Они преобразуют ленивую последовательность в массив. Отметьте, что следующий код эквивалентен предыдущему.

string[] oo = words.Where(c => c.Contains("oo")).ToArray<string>();

Вместо массива вы можете преобразовать выходную последовательность в generic-список. Для этого надо воспользоваться extension-методом ToList() или ToList<string>().

List<string> ooList = words.Where(c => c.Contains("oo")).ToList();

Методы Select и SelectMany

Рассмотрим, как работают методы Select и SelectMany, которые позволяют проецировать данные на удобные нам структуры. Тип элементов выходной последовательности может отличаться от типа данных входной. Методы Select и SelectMany требуют в качестве параметра задать адрес трансформирующей функции. Она называется селектором (selector) и должна создавать одно значение для каждого значения перебираемой последовательности.

Запомните. Метод Where требует задать предикат, а методы Select и SelectMany требуют задать селектор.

Если селектор возвращает значение, которое, в свою очередь, является последовательностью, то нам необходимо написать код прохода по ней. В случае использования SelectMany этого делать не надо, так как сам метод SelectMany обрабатывает последовательность возвращаемых последовательностей. Он сливает их в одну общую последовательность. Рассмотрим пример, который иллюстрирует использование обоих методов для вывода результатов экзаменов для массива студентов. Пусть каждый студент задан объектом класса Stud с таким набором автогенерируемых свойств.

class Stud{public string Name { get; set; }public int[] Marks { get; set; }

}

Массив студентов можно задать таким образом.

Stud[] studs = { new Stud { Name="John Best", Marks = new[]{ 5, 5, 5, 5, 5 } },new Stud { Name="Joy Amore", Marks = new[]{ 4, 4, 3, 4 } },new Stud { Name="Jim Lowson", Marks = new[]{ 3, 3, 2 } },

};

Далее мы создаем отложенный запрос с помощью метода Select.

var q = studs.Select (s => s.Marks); // Тип переменной q - IEnumerable<int[]>

Запрос создает объект класса, реализующего интерфейс IEnumerable<int[]>. Это означает, что запрос возвратит последовательность массивов, каждый их которых представляет собой перечень оценок текущего студента типа int[]. Мы пользуемся удобной

Page 25: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

возможностью задать тип запроса (то есть, тип переменной q) с помощью ключевого слова var. Но компилятор должен вычислить тип T параметра шаблона IEnumerable<T>. Он анализирует λ-выражение s => s.Marks и приходит к выводу, что нужен параметр типа int[].

Теперь можно перечислить элементы списка списков, возвращаемого запросом q. Для этого недостаточно одного прохода, поэтому используются два, вложенных цикла.

int i=0;Console.WriteLine ("Student\t\t Mark\n" + new string('\u2500', 31));foreach (var array in q){Console.Write (studs[i++].Name + ":\t ");foreach (int m in array)Console.Write (m + ", ");

Console.WriteLine();}

Альтернативный вариант предыдущего примера может быть получен путем замены массива на список.

При этом перечень оценок текущего студента будет иметь тип List<int>. Тип запроса будет иметь тип IEnumerable<List<int>>. В класс Stud место int[] Marks { get; set; } подставьте List<int> Marks { get; set; } Вместо строк вида: Marks = new[] { 5, 5, 5, 5, 5 } }, подставьте Marks = new List<int>{ 5,

5, 5, 5, 5 } }. Остальные строки кода можно оставить неизменными.

Итак, метод Select в применении к последовательности объектов Stud возвращает коллекцию коллекций. Рассморим, тип возвращаемой последовательности при использовании метода SelectMany. Так как этот метод соединяет все подпоследовательности в одну, то ее тип будет — IEnumerable<int>. Для перечисления такого результата (отложенного) запроса достаточно написать лишь один цикл.

var query = studs.SelectMany(s => s.Marks); // Тип переменной q - IEnumerable<int>Console.Write ("\nAll the Marks: ");foreach (var m in query)Console.Write (m + ", ");

Говорят, что SelectMany — проекция, имеющая тип one-to-many. При проходе по последовательности она заменяет каждый список множеством его значений (сливает списки в одну последовательность).

Существует еще пара версий методов Select и SelectMany. Они, также, как и в случае с Where, работают с делегатным типом, требующим три параметра Func<T,int,S>. Рассмотрим пример.

static void TestNumbers(){int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };var q = numbers.Select ((n, i) => new { Value = n, InPlace = (n == i) });

Console.WriteLine("Number In-place?\r\n" + new string('\u2500', 20));foreach (var v in q)Console.WriteLine("{0,-10} {1}", v.Value, v.InPlace);

}

Здесь метод Select возвращает не коллекцию коллекций, а последовательность анонимных объектов. В таких случаях принято говорить о проецировании множества целых чисел (массива) на множество объектов с именованными свойствами.

Каждый объект результирующего множества имеет свойства: int Value, bool InPlace. Свойство InPlace равно true для тех чисел, местоположение в массиве которых совпадает с их значением. Это справедливо для чисел: 3, 6, 7 (учтите, что число 5 занимает нулевую позицию). Напомним, что второй параметр левой части λ-выражения (n, i), как и ранее в методе Where, является индексом элемента в массиве.

В данном случае найти замену описателю var при переменной q найти не удается. Отладчик сообщает, что его типом является IEnumerable<<>f__AnonymousType0<int,bool>>. Описатель лишь приоткрывает

Page 26: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

внутреннюю кухню LINQ, но не воспринимается компилятором C#, если вы попытаетесь заменить им var.

Последовательность анонимных объектов генерируется в отложенном процессе и имеет конкретное внешнее представление. Например, первый элемент этой последовательности отображается таким образом.

v = { Value = 5, InPlace = false }

Следующий пример иллюстрирует работу цепочки extension-методов. Сначала с массивом слов работает метод Select, который превращает его в последовательность анонимных объектов. Затем выход метода Select подается на вход метода OrderBy, который сортирует последовательность по длине параметра c. Этот параметр имеет тип string и обозначает текущий элемент исходного массива строк.

static void TestSelectAnonimous(){string[] words = { "abstract", "false", "finally", "null", "for", "if", "object", "Freeloader" };var q = words.Select((c, i) => new { ID = i + 1, Length = c.Length, Word = c }).OrderBy(c=>c.Length);

foreach (var v in q)Console.WriteLine(v);

}

Мы рассмотрели методы проецирования Select и SelectMany и показали, что трансформирующая функция (selector) может быть задана λ-выражением, проецирующим множество элементов массива на множество анонимных объектов.

Методы Single, SingleOrDefault, First, FirstOrDefault, Last, LastOrDefault

Эти методы позволяют вернуть не коллекцию IEnumerable или IQueryable, а одно единственное значение, выбранное из последовательности. Например.

string[] words = { "abstract", "false", "finally", "null", "for", "if", "object", "Freeloader" };Console.WriteLine(words.First(s => s[0] == 'f'));Console.WriteLine(words.Last(s => s.StartsWith("f", StringComparison.InvariantCultureIgnoreCase)));

Если коллекция пуста, то методы First и Single выбросят исключение. Методы FirstOrDefault и SingleOrDefault не сделают этого и вернут значение по умолчанию, которым обычно является null. Например.

int?[] ia = new int?[1];int? n = ia.FirstOrDefault(c => (c & 1) == 1);Console.WriteLine(n == null ? "null" : n.ToString());ia = new int?[] { 6, 8, 9};n = ia.FirstOrDefault(c => (c & 1) == 1);Console.WriteLine(n == null ? "null" : n.ToString());

Если коллекция содержит более одного элемента, то методы Single и SingleOrDefault выбросят исключение. Поэтому их следует использовать вместе с фильтром Where, который гарантирует единственность выбора. Например.

Console.WriteLine(ia.Where(c => c % 4 == 0).Single());int? divBy7 = ia.Where(c => c % 7 == 0).SingleOrDefault();Console.WriteLine(divBy7 == null ? "null" : divBy7.ToString());

Сортировка данных

Extension-метод OrderBy имеет две сигнатуры, одна из которых имеет следущий вид.

public static IOrderedEnumerable<T> OrderBy<T, TKey>(this IEnumerable<T> source,Func<T, TKey> keySelector);

Этот метод позволяет упорядочить входную последовательность source с помощью generic-делегата keySelector, который для каждого элемента последовательности возвращает ключ сортировки. Например, пусть задан класс Person с таким набором автогенерируемых свойств.

public class Person{public string Name { get; set; }public DateTime Birth { get; set; }public int Status { get; set; }

}

Page 27: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

Объявим в классе Program статический массив объектов этого класса.

static Person[] personList = {new Person{Name = "Paul", Birth = DateTime.Now.AddYears(-15), Status = 1 },new Person{Name = "Alex", Birth = DateTime.Now.AddYears(-45), Status = 17 },new Person{Name = "Peter", Birth = DateTime.Now.AddYears(-50), Status = 21 },new Person{Name = "John", Birth = DateTime.Now.AddYears(-35), Status = 8 },new Person{Name = "Zorro", Birth = DateTime.Now.AddYears(-25), Status = 8 },new Person{Name = "Alex", Birth = DateTime.Now.AddYears(-15), Status = 12 },new Person{Name = "Alex", Birth = DateTime.Now.AddYears(-45), Status = 12 }

};

Используем extension-метод OrderBy для упорядочивания элементов по возрастанию статуса. Для этого достаточно вызвать OrderBy для массива personList и подать на вход λ-выражение, определяющее поле ключа сортировки.

static void TestOrderBy(){var q = personList.OrderBy(c => c.Status);Console.WriteLine("\nPersons ordered by Status:");foreach (var p in q)Console.WriteLine("{0} {1} {2}", p.Name.PadRight(7),p.Birth.ToShortDateString(), p.Status);

}

Для сортировки в порядке убывания надо заменить метод OrderBy на OrderByDescend-ing. Вторые версии указанных методов имеют другую сигнатуру. Например, метод OrderBy выглядит так:

public static IOrderedEnumerable<T> OrderBy<T, TKey>(this IEnumerable<T> source,Func<T, TKey> keySelector, IComparer<TKey> comparer);

При вызове этой версии метода следует задать два параметра: generic-делегат keySelector и адрес функции сравнения. Последняя должна быть методом класса, реализующего generic-интерфейс IComparer<T>. Для того, чтобы показать различные варианты сортировки, объявим три таких класса.

public class NameComparer : IComparer<Person>{public int Compare(Person p, Person q){int n = p.Name.CompareTo(q.Name),

b = p.Birth.CompareTo(q.Birth),s = p.Status.CompareTo(q.Status);

return n != 0 ? n : b != 0 ? b : s;}

}

Назовем второй класс AgeComparer, он будет упорядочивать поля по дате рождения, затем по имени, затем по статусу. Третий класс, StatusComparer, будет упорядочивать поля по статусу, затем по имени, затем по дате рождения. Код классов вы разработаете самостоятельно. Теперь приведем код, который показывает, как использовать вторую версию метода OrderBy для сортировки массива по различным полям.

q = personList.OrderBy (c => c, new NameComparer());

Console.WriteLine("\nPersons ordered by Name, Age, Status:");foreach (var p in q)Console.WriteLine("{0} {1} {2}", p.Name.PadRight(7), p.Birth.ToShortDateString(), p.Status);

q = personList.OrderBy (c => c, new AgeComparer());Console.WriteLine("\nPersons ordered by Age, Name, Status:");. . . . .

Результат, полученный с помощью любого из классов *Comparer, можно получить и другим способом. Например, с помощью вызова метода OrderBy, а затем двухкратного вызова ex-tension-метода ThenBy.

q = personList.OrderBy(c=>c.Status).ThenBy(c=>c.Name).ThenBy(c=>c.Birth);

Также просмотрите справку по методу ThenByDescending.

Page 28: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

Аккумулирование данных

Рассмотрим extension-метод Aggregate, который применяет аккумулирующую функцию для всех элементов какой-либо последовательности.

double[] doubles = { 1, 2, 3, 4, 5 };double product = doubles.Aggregate((a, b) => a * b);Console.WriteLine ("5!: " + product);

Этот фрагмент вычислит и выведет факториал числа 5, так как в качестве аккумулирующей функции задано λ-выражение, которое перемножает два соседних элемента.

Здесь использована одна из версий метода Aggregate (с одним параметром). Она перемещает шаблон (a, b) вдоль всей последовательности (в нашем случае, массива doubles), применяет к текущей паре элементов подстановку (a*b) и аккумулирует результат в переменной product. Другие версии метода Aggregate требуют задать большее количество параметров, и позволяют выполнить более сложные манипуляции с элементами последовательности. Например:

double sum = 10000;int[] withdrawals = { 2000, 1000, 4000, 5000 };

double res = withdrawals.Aggregate(sum, (b, s) => s <= b ? b - s : b);Console.WriteLine("Ending balance: {0}", res);

Массив withdrawals описывает последовательность попыток снятия денег с банковского счета.

λ-выражение, определяющее аккумулирующую функцию, задано так, чтобы исключить возможность снятия со счета суммы, которая превышает остаток.

b — текущий баланс (остаток на счете). s — текущая попытка снять какую-то сумму денег (текущий элемент массива

withdrawals).

Результат res применения метода Aggregate к коллекции withdrawals будет 3000, так как последняя попытка снять 5000 не пройдет проверки s <= b.

Методы Union, Intersect, Except и Count

Рассмотрим еще несколько расширяющих методов: Union, Intersect, Except и Count. Первые три работают с парой последовательностей, а последний — с одной.

Union (объединение) вычисляет множество общих элементов последовательностей, Intersect — множество пересекающихся элементов, Except — множество различий. Метод Count возвращает количество элементов в последовательности.

int[] u = { 1, 2, 3, 4, 5, 6 },v = { 4, 5, 6, 7, 8, 9, 10 };

var both = u.Intersect(v);Console.WriteLine("\nu.Intersect(v)");foreach (var n in both)Console.Write(n + ", ");

var r = both.Except(v.Intersect(u));Console.WriteLine("\nu.Intersect(v).Except(v.Intersect(u)).Count(): " + r.Count());Console.WriteLine("\nu.Union(v).Except(u)");foreach (var n in u.Union(v).Except(u))Console.Write(n + ", ");

Запустите код и убедитесь, что различий в пересечениях u.Intersect(v) и v.Intersect(u) нет, а выражение u.Union(v).Except(u) вычисляет множество элементов второй последовательности за вычетом элементов, общих для u и v.

Мы рассмотрели лишь малую часть методов-расширений для класса Enumerable, которые реализованы в пространстве имен System.Linq. Все эти методы являются основой построения интегрированных запросов, которые можно выполнять как путем прямого вызова extension methods, так и с помощью специальных синтаксических конструкций языка C#.

Page 29: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

Интегрированные запросы

Синтаксис интегрированных запросов

Рассмотрим пример, который выполняет один и тот же запрос, но использует при этом два различных подхода. В первом подходе запрос осуществляет прямой вызов метода Where, а во втором — использует новые операции языка C#: from, where и select. В документации MSDN идентификаторы операций where и select языка C# (в отличие от методов Where и Select) часто сопровождают уточняющим описателем clause (операция).

В первом варианте примера мы намеренно не пользуемся λ-выражением с целью показать, как в действительности работает внутренняя кухня LINQ.

Во втором варианте, наоборот, включаются все средства минимизации выражений, присущие LINQ.

Как было сказано ранее, компилятор заменяет λ-выражение кодом, который вызывает функцию. Эта функция соответствует одному из делегатных типов семейства Func<>. Код первого варианта выполняет это действие явным образом, а второго — неявным. Итак, для реализации первого варианта надо:

Создать метод, соответствующий делегатному типу Func<T, TResult>, Создать последовательность объектов типа T, Создать LINQ-запрос, вызвав, например, extension-метод Where (Func<T, TResult>), Создать перечислитель IEnumerable<T>, настроенный на выбранный ранее тип, Перечислить выходную последовательность, генерируемую при выполнении

отложенного запроса.

Следующий кодовый фрагмент реализует намеченный план. В качестве предиката, который следует задать при вызове метода Where, мы используем метод IsLeapYear, проверяющий целое число на соответствие високосным годам.

static bool IsLeapYear(int y) { bool res = y % 4 == 0 && y % 100 != 0 || y % 400 == 0;return res;

}

static void TestLeapYear(){Console.WriteLine("\nTest Verbose version of IsLeapYear Query");var years = new int[] { 2000, 2001, 2002, 2003, 2004, 2005 };Func<int, bool> tester = IsLeapYear;

IEnumerable<int> result = years.Where<int>(tester);IEnumerator<int> en = result.GetEnumerator();while (en.MoveNext())Console.WriteLine(en.Current);

}

Теперь рассмотрим код второго варианта этого же запроса. Он использует операции from, where и select, встроенные в язык C# 3.5. С их помощью реализуются интегрированные запросы.

Console.WriteLine("\nConcise Version of IsLeapYear Query");var q = from y in Enumerable.Range(2000, 6) where (y % 4 == 0 && y % 100 != 0 || y % 400 == 0) select y;foreach (var v in q) Console.WriteLine(v);

Этот запрос реализует тот же самый алгоритм — отбирает из массива целых чисел только те, которые соответствуют високосным годам. Согласитесь, что второй вариант намного проще создать, прочесть и при желании модифицировать. Справедливости ради, следует отметить, что в таком простом примере на основе extension-метода можно получить еще более краткое выражение.

result = years.Where(c => c % 4 == 0 && c % 100 != 0 || c % 400 == 0); // Вариант запроса номер триforeach (var v in result)Console.WriteLine(v);

Page 30: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

Заметьте, что интегрированные запросы более дружелюбны. Компилятор превращает их в вызовы extension-методов. C некоторыми допущениями можно сказать, что компилятр языка C# превращает второй вариант запроса в первый.

Синтаксис оператора from x in коллекция выстроен так, чтобы задать переменную x для прохода вдоль всей последовательности (или коллекции). Эту переменную называют псевдонимом (alias) или управляющей переменной. Заметим, что оператор from не имеет соответствующего extension-метода.

Вы заметили, что, в отличие от обычных SQL-запросов, операторы select и from стоят не на своих местах. Благодаря такому решению, появляется возможность включить механизм In-tellisense. Как только стала известна коллекция (после предложения from x in коллекция), начинают работать метаданные, и, включаются подсказки. Такой подход значительно упрощает и ускоряет процесс разработки приложения.

Рассмотрим еще два варианта выполнения одного и того же запроса. Первый — использует extension-метод Select, а второй — встроенную операцию select. На сей раз запрос проецирует диапазон целых чисел на множество анонимных объектов. Вот код первого варианта:

var q = Enumerable.Range(20,10).Select(n => new { Value = n, Mult3 = n % 3 == 0 });

Заметьте, что здесь необходимо использовать λ-операцию =>. Теперь рассмотрим второй вариант запроса, который обходится без λ-операции, но требует дополнить select двумя другими операциями (from и in).

q = from n in Enumerable.Range(20, 10) select new { Value = n, Mult3 = n % 3 == 0 };

Для вывода результатов запросов воспользуемся уже опробованным приемом.

Console.WriteLine("\nNumber Mult3?\r\n" + new string('\u2500', 21));foreach (var v in q)Console.WriteLine("{0,-10} {1}", v.Value, v.Mult3);

Сортировка

Встроенная операция orderby также имеет аналог в виде extension-метода OrderBy. Покажем, как они работают на примере сортировки массива анонимных объектов.

var people = new[]{new {Name="Alex Black", Position="Developer", Birth=new DateTime(1980,2,26) },new {Name="Alex Ritter", Position="Boss", Birth=new DateTime(1969,3,15) },new {Name="Peter Pann", Position="Manager", Birth=new DateTime(1980,9,12) },new {Name="Joy Amore", Position="Referent", Birth=new DateTime(1990,6,27) },new {Name="Jack O'Mule", Position="Referent", Birth=new DateTime(1990,4,2) },new {Name="Jill Sweet", Position="Referent", Birth=new DateTime(1990,9,20) },new {Name="Brad Show", Position="Developer", Birth=new DateTime(1980,2,26) }

};

var q = from p in people orderby p.Position descending, p.Name select p;

foreach (var v in q)Console.WriteLine(v);

Последовательность объектов сортируется сначала по убыванию поля Position, затем по возрастанию поля Name. Вариант с использованием extension-методов выглядит следующим образом.

q = people.OrderByDescending (p => p.Position).ThenBy (p => p.Name);

Вы видите, что подход с ипользованием extension-методов по краткости не уступает подходу на основе интегрированных запросов, то есть, встроенных операций: from, select, orderby, descending, но он требует умения писать λ–выражения.

Группировка

Встроенные операции: group и by, а также их аналог в виде extension-метода GroupBy, позволяют создать группы объектов. Результат группировки — коллекция коллекций, то есть, последовательность групп, где каждая группа — это последовательность объектов. Каждой группе присваивается ключ (свойство Key). Для перечисления элементов групп

Page 31: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

нобходимо создать два вложенных цикла. Внешний цикл по группам (различаемым ключами), внутренний — по элементам внутри каждой группы.

var numbers = new [] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };var gr = from n in numbers group n by n % 3;

Console.WriteLine("Test grouping");foreach (var g in gr.ToArray()) // Вместо var можно подставить тип IGrouping<int, int>{Console.Write("\n Group # " + g.Key + ": ");foreach (var v in g.ToArray())Console.Write(v + ", ");

}

Пример выше разбивает массив целых чисел на три группы в зависимости от остатка деления на 3. Двойной цикл выводит все элементы с учетом деления на группы.

Test grouping

Group # 1: 1, 4, 7, 10, Group # 2: 2, 5, 8, Group # 0: 3, 6, 9,

Анализируя код, обратите внимание на свойство Key.

Задание. Попробуйте вычислить тип переменной gr (то есть, тип самой группы).

Ответ. Описатель var можно заменить описателем типа: IEnumerable<IGrouping<int,int>>. Если вывести на консоль имя типа, например: Console.WriteLine(gr.GetType().Name);, то вы увидите внутреннее имя типа: "GroupedEnumerable`3". Этим именем пользуется компилятор и здесь мы видим намек на три параметра. Если в окне отладчика проверить истинность утверждения: gr.GetType() == typeof(System.Linq.IGrouping<int, int, int>), то мы получим true. Но при попытке вставить это выражение в код программы компилятор выдаст ошибку.

Задание. Вычислите тип переменной g. Ответ. IGrouping<int, int>.

Следующий фрагмент реализует тот же самый запрос, но с помощью extension-метода GroupBy.

gr = numbers.GroupBy (n => n % 3); // Он заметно короче

Теперь покажем, как заменить цикл перебора элементов extension-методом ForEach.

Console.WriteLine("\nTest grouping (Extension)");Array.ForEach (gr.ToArray(), x =>{Console.Write("\n Group # " + x.Key + ": ");Array.ForEach (x.ToArray(), y => Console.Write(y + ", "));

});

Обратите внимание на следующие детали:

Перечисление результатов группировки требует вызвать ForEach дважды. Для прохода вдоль последовательности gr, имеющей тип IEnumerable<IGroup-

ing<int,int>>, необходимо привести ее к типу Array. Это делает extension-метод ToArray. Для прохода вдоль последовательности g, имеющей тип IGrouping<int,int>, также

необходимо привести ее к типу Array. Это также делает extension-метод ToArray. Первая лямбда-операция (читай — код делегата) имеет более сложный вид, чем вторая.

Блок кода, реализующий ее, содержит вложенную лямбда-операцию y => Console.Write(y + ", ").

Лямбда-выражения могут быть составными. Следующий пример показывает, что λ-выражение с двумя параметрами эквивалентно последовательности двух λ-выражений с одним параметром.

Console.Write("\nTest (x, y) => x + y: ");Func<int, int, int> lambda = (x, y) => x + y;Console.WriteLine (lambda(3, 4));Func<int, Func<int, int>> curry = x => y => x + y;Console.Write ("Test x => y => x + y: ");Console.WriteLine (curry(3)(4));

Page 32: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

Интерфейс IQueryable <T>

Мы убедились, что источником данных для LINQ-запроса может быть любая коллекция, реализующая интерфейс IEnumerable<T>. Но им также может быть и любой объект, реализующий интерфейс IQueryable<T>.

public interface IQueryable<out T> : IEnumerable<T>, IQueryable, IEnumerable

Параметр T является ковариантным. Это означает, что вместо типа T вы можете подать и любой из типов, производных от T. Типы, реализующие IQueryable<T>, не являются коллекциями. Они представляют собой описания запросов, которые генерируются динамически и поддерживают полиморфизм и оптимизацию. Такие типы, совместно с деревьями выражений (expression trees) допускают компиляцию запросов с целью повышения производительности, и могут служить входными данными для образования других LINQ-запросов.

Интерфейс IQueryable (так же как и IEnumerable) задает сигнатуру только одного метода.

IEnumerator GetEnumerator()

Интерфейс IQueryProvider определяет методы, необходимые для создания и исполнения запросов, определенных IQueryable-типом. Интерфейс IQueryProvider задает сигнатуру двух методов:

IQueryable CreateQuery(Expression expression);object Execute <TResult>(Expression)

Цепочка действий, которая выполняется при вызове метода Execute <TResult>(Expression) зависит от провайдера данных. Одним из вариантов реализации запроса может быть трансляция дерева выражения в текст запроса, удовлетворяющего синтаксису конкретного языка запросов. Конкретный язык запросов определяется провайдером данных.

COM Interop

Следующий пример показывает, как используются новые возможности C# 4.0 для управления объектами COM Interop. В частности для создания документов Microsoft Office (Excel и Word). Новые возможности (именованные параметры и индексируемые свойства) сильно упрощают общение с Office API.

using System;using System.Collections.Generic;using System.Drawing;using Excel = Microsoft.Office.Interop.Excel;using Word = Microsoft.Office.Interop.Word;

public class Account{public int ID { get; set; }public double Balance { get; set; }

}

public class Program{static void Main(){var checkAccounts = new List<Account>{new Account { ID = 345, Balance = 541.27 },new Account { ID = 123, Balance = -127.44 }

};

Action<Account, Excel.Range> display = (account, cell) =>{cell.Value = account.ID;cell.Offset[0, 1].Value = account.Balance;if (account.Balance < 0){cell.Interior.Color = ColorTranslator.ToOle(Color.FromArgb(255,255,200));cell.Offset[0, 1].Interior.Color = ColorTranslator.ToOle(Color.LightSkyBlue);

}};

DisplayInExcel(checkAccounts, display);

Page 33: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

var word = new Word.Application();word.Visible = true;word.Documents.Add();word.Selection.PasteSpecial(Link: true, DisplayAsIcon: false);

}

public static void DisplayInExcel(IEnumerable<Account> accounts, Action<Account, Excel.Range> display){var xl = new Excel.Application();

xl.Workbooks.Add();xl.Visible = true;xl.Cells[1, 1] = "ID";xl.Cells[1, 2] = " Balance";xl.Cells[2, 1].Select();foreach (var ac in accounts){display(ac, xl.ActiveCell);xl.ActiveCell.Offset[1, 0].Select();

}

xl.Range["A1:B3"].Copy(); // xl.get_Range("A1:B3").Copy();xl.Columns[1].AutoFit();xl.Columns[2].AutoFit();

}}

Для того, чтобы оценить вклад именованных параметров и параметров по умолчанию, сравните сигнатуру метода PasteSpecial с тем, как он был в действительности вызван в нашем коде.

void PasteSpecial(ref object IconIndex = null, ref object Link = null, ref object Placement = null, ref object DisplayAsIcon = null, ref object DataType = null, ref object IconFileName = null, ref object IconLabel = null);

Новый тип данных dynamic также упрощает работу с Office COM Interop. Типы, используемые в Office представлены в C# как dynamic. Без использования типа dynamic вам пришлось бы работать так:

((Excel.Range)excel.Cells[1, 1]).Value2 = "ID";

С использованием этого типа мы работаем так:

X1.Cells[1, 1].Value = "ID";

Индексируемые свойства (Index Properties) упрощают обращение к ячейке до:

xl.Cells[1, 1] = "ID";

При работе с COM Interop сборки вида Primary Interop Assemblies (PIA) генерируются на основе COM-интерфейсов и упрощают разработку кода, но они сильно раздувают конечный продукт, так как содержат массу (возможно) неиспользуемых функций. Компилятор C# 4.0 позволяет имеет настройку No-PIA, которая позволяет включать в конечный код (distribution module) только используемую часть PIA.

How to: Detect and Resolve Conflicting Submissions

Northwnd db = new Northwnd(@"c:\northwnd.mdf");

Customer newCust = new Customer();newCust.City = "Auburn";newCust.CustomerID = "AUBUR";newCust.CompanyName = "AubCo";db.Customers.InsertOnSubmit(newCust);

try{ db.SubmitChanges(ConflictMode.ContinueOnConflict);}catch (ChangeConflictException e){ Console.WriteLine("Optimistic concurrency error."); Console.WriteLine(e.Message);

Page 34: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

Console.ReadLine(); foreach (ObjectChangeConflict occ in db.ChangeConflicts) { MetaTable metatable = db.Mapping.GetTable(occ.Object.GetType()); Customer entityInConflict = (Customer)occ.Object; Console.WriteLine("Table name: {0}", metatable.TableName); Console.Write("Customer ID: "); Console.WriteLine(entityInConflict.CustomerID); foreach (MemberChangeConflict mcc in occ.MemberConflicts) { object currVal = mcc.CurrentValue; object origVal = mcc.OriginalValue; object databaseVal = mcc.DatabaseValue; MemberInfo mi = mcc.Member; Console.WriteLine("Member: {0}", mi.Name); Console.WriteLine("current value: {0}", currVal); Console.WriteLine("original value: {0}", origVal); Console.WriteLine("database value: {0}", databaseVal); } }}catch (Exception ee) // Catch other exceptions{ Console.WriteLine(ee.Message);}finally{ Console.WriteLine("TryCatch block has finished.");}

How to: Resolve Concurrency Conflicts by Retaining Database Values

In this scenario, a ChangeConflictException exception is thrown when User1 tries to submit changes, because User2 has in the meantime changed the Assistant and Department columns. The following table shows the situation.

Manager Assistant Department

Original database state when queried by User1 and User2. Alfreds Maria Sales

User1 prepares to submit these changes. Alfred Marketing

User2 has already submitted these changes. Mary Service

User1 decides to resolve this conflict by having the newer database values overwrite the current values in the object model. When User1 resolves the conflict by using OverwriteCurrentValues, the result in the database is as follows in the table:

Manager Assistant Department

New state after conflict resolution. Alfreds

(original)

Mary

(from User2)

Service

(from User2)

The following example code shows how to overwrite current values in the object model with the database values. (No inspection or custom handling of individual member conflicts occurs.)

Northwnd db = new Northwnd("...");try { db.SubmitChanges(ConflictMode.ContinueOnConflict); }catch (ChangeConflictException e){Console.WriteLine(e.Message);foreach (ObjectChangeConflict occ in db.ChangeConflicts)occ.Resolve(RefreshMode.OverwriteCurrentValues); // All database values overwrite current values

}

How to: Resolve Concurrency Conflicts by Overwriting Database Values

User1 decides to resolve this conflict by overwriting database values with the current client mem-ber values. When User1 resolves the conflict by using KeepCurrentValues, the result in the database is as in following table:

Manager Assistant Department

New state after conflict resolution. Alfred

(from User1)

Maria

(original)

Marketing

(from User1)

Page 35: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

The following example code shows how to overwrite database values with the current client mem-ber values. (No inspection or custom handling of individual member conflicts occurs.)

try { db.SubmitChanges(ConflictMode.ContinueOnConflict); }catch (ChangeConflictException e){Console.WriteLine(e.Message);foreach (ObjectChangeConflict occ in db.ChangeConflicts)occ.Resolve(RefreshMode.KeepCurrentValues); //No database values are merged into current

}

How to: Resolve Concurrency Conflicts by Merging with Database Values

User1 decides to resolve this conflict by merging database values with the current client member values. The result will be that database values are overwritten only when the current changeset has also modified that value. When User1 resolves the conflict by using KeepChanges, the result in the database is as in the following table:

Manager Assistant Department

New state after conflict resolution. Alfred

(from User1)

Mary

(from User2)

Marketing

(from User1)

The following example shows how to merge database values with the current client member val-ues (unless the client has also changed that value). No inspection or custom handling of individual member conflicts occurs.

try { db.SubmitChanges(ConflictMode.ContinueOnConflict); }catch (ChangeConflictException e){Console.WriteLine(e.Message);foreach (ObjectChangeConflict occ in db.ChangeConflicts) occ.Resolve(RefreshMode.KeepChanges); // Automerge database values for members that client has not modified

}db.SubmitChanges(ConflictMode.FailOnFirstConflict); // Submit succeeds on second try

How to: Display Generated SQL

db.Log = Console.Out;

You can view changes tracked by a DataContext by using GetChangeSet.

Northwnd db = new Northwnd(@"c:\northwnd.mdf");var custQuery =from cust in db.Customerswhere cust.City == "London"select cust;

foreach (Customer c in custQuery){Console.WriteLine("CustomerID: {0}", c.CustomerID);Console.WriteLine("\tOriginal value: {0}", c.City);c.City = "Paris";Console.WriteLine("\tUpdated value: {0}", c.City);

}ChangeSet cs = db.GetChangeSet();Console.Write("Total changes: {0}", cs);Console.ReadLine(); // Freeze the console windowdb.SubmitChanges();

How to: Display LINQ to SQL Commands

In the following example, the console window displays the output from the query, followed by the SQL commands that are generated, the type of commands, and the type of connection.

Northwnd db = new Northwnd(@"c:\northwnd.mdf");var q =from cust in db.Customerswhere cust.City == "London"select cust;

Console.WriteLine("Customers from London:");foreach (var z in q)Console.WriteLine("\t {0}",z.ContactName);

Page 36: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

DbCommand dc = db.GetCommand(q);Console.WriteLine("\nCommand Text: \n{0}",dc.CommandText);Console.WriteLine("\nCommand Type: {0}",dc.CommandType);Console.WriteLine("\nConnection: {0}",dc.Connection);

Page 37: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

Visitor pattern

In object-oriented programming and software engineering, the visitor design pattern is a way of separating an algorithm from an object structure upon which it operates. A practical result of this separation is the ability to add new operations to existing object structures without modifying those structures. Thus, using the visitor pattern helps conformance with the open/closed principle.

In essence, the visitor allows one to add new virtual functions to a family of classes without modi-fying the classes themselves; instead, one creates a visitor class that implements all of the appro-priate specializations of the virtual function. The visitor takes the instance reference as input, and implements the goal through double dispatch. While powerful, the visitor pattern is more limited than conventional virtual functions. It is not possible to create visitors for objects without adding a small callback method inside each class. In naive implementations, the callback method in each of the classes is not inheritable.

Details

The idea is to use a structure of element classes, each of which has an accept() method that takes a visitor object as an argument. Visitor is an interface that has a visit() method for each element class. The accept() method of an element class calls back the visit() method for its class. Separate concrete visitor classes can then be written that perform some particular operations, by imple-menting these operations in their respective visit() methods.

One of these visit() methods of a concrete visitor can be thought of as a method not of a single class, but rather a method of a pair of classes: the concrete visitor and the particular element class. Thus the visitor pattern simulates double dispatch in a conventional single-dispatch object-oriented language such as Java, Smalltalk, and C++. For an explanation of how double dispatch differs from function overloading, see Double dispatch is more than function overloading in the double dispatch article. In the Java language, two techniques have been documented which use reflection to simplify the mechanics of double dispatch simulation in the visitor pattern: getting rid of accept() methods (the Walkabout variation), and getting rid of extra visit() methods.

The visitor pattern also specifies how iteration occurs over the object structure. In the simplest version, where each algorithm needs to iterate in the same way, the accept() method of a con-tainer element, in addition to calling back the visit() method of the visitor, also passes the visitor object to the accept() method of all its constituent child elements.....

Because the Visitor object has one principal function (manifested in a plurality of specialized methods) and that function is called visit(), the Visitor can be readily identified as a potential func-tion object. Likewise, the accept() function can be identified as a function applicator, a mapper, which knows how to traverse a particular type of object and apply a function to its elements. Lisp's object system with its multiple dispatch does not replace the Visitor pattern, but merely provides a more concise implementation of it in which the pattern all but disappears.

class SpaceShip {};class GiantSpaceShip : public SpaceShip {}; class Asteroid {public: virtual void CollideWith(SpaceShip&) { cout << "Asteroid hit a SpaceShip" << endl; } virtual void CollideWith(GiantSpaceShip&) { cout << "Asteroid hit a GiantSpaceShip" << endl; }}; class ExplodingAsteroid : public Asteroid {public: virtual void CollideWith(SpaceShip&) { cout << "ExplodingAsteroid hit a SpaceShip" << endl; } virtual void CollideWith(GiantSpaceShip&) {

Page 38: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

cout << "ExplodingAsteroid hit a GiantSpaceShip" << endl; }};

Suppose SpaceShip and GiantSpaceShip both have the function

virtual void CollideWith(Asteroid& asteroid) { asteroid.CollideWith(*this); }

Page 39: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

Глава из книги "C# 2008: ускоренный курс для профессионалов"

Автор: Трей Нэш

Большинство новых средств C# 3.0 открывают программистам на C# мир выразительного функционального программирования. Функциональное программирование в его чистом виде — это методология программирования, построенная поверх неизменяемых переменных (иногда называемых символами), функций, которые могут производить другие функции, и рекурсии; и это лишь несколько его основ. К выдающимся языкам функционального программирования можно отнести Lisp, Haskell, F# и Scheme. Однако функциональное программирование не требует специального функционального языка, и вы можете с успехом реализовать его на традиционных императивных языках, таких как все C-подобные языки (включая C#). Новые средства C# 3.0 трансформируют язык в более выразительный гибридный язык, в котором приемы императивного и функционального программирования сосуществуют в гармонии. Лямбда-выражения —несомненно, самый большой кусок этого пирога функционального программирования.

F# – выдающийся новый язык функционального программирования для .NET Framework. Подробную информацию об этом языке читайте в книге Роберта Пикеринга (Robert Pickering) Foundations of F# (Berkeley, CA: Apress, 2007 г.). Один из языков, которые я часто использую — C++. Те из вас, кто знаком с метапрограммированием на C++, определенно знакомы и с приемами функционального программирования. Если вы используете C++ и интересуетесь метапрограммированием, загляните в блестящую книгу Дэвида Абрахамса (David Abrahams) и Алексея Гуртового (Aleksey Gurtovoy) C++ Template Metaprogramming: Concepts, Tools, and Techniques from Boost and Beyond (Boston, MA: Addison-Wesley Professional, 2004 г.).

Введение в лямбда-выражения

Используя лямбда-выражения, вы можете кратко определять функциональные объекты для использования в любое время. C# всегда поддерживал эту возможность через делегаты, посредством которых вы создаете функциональный объект (в форме делегата) и привязываете к нему код обратного вызова во время создания. Лямбда-выражения связывают эти два действия (создание и подключение) в один выразительный оператор кода. Вдобавок вы можете легко ассоциировать среду с функциональными объектами. Функционал (functional) — это функция, принимающая функции в своем списке параметров и оперирующая этими функциями, возможно, даже возвращая другую функцию в результате. Например, функционал может принимать две функции, одна из которых выполняет одну математическую операцию, а другая — другую математическую операцию, и возвращать третью функцию, представляющую комбинацию первых двух. Лямбда-выражения предоставляют более естественный способ создания и вызова функционалов.

В простых синтаксических терминах лямбда-выражение — это синтаксис, посредством которого вы можете объявлять анонимные функции (делегаты) более гладким и выразительным способом. Вообще говоря, нет причин, почему бы нельзя было реализовать технику функционального программирования на C# 2.0. Во-первых, синтаксис лямбда-выражений может потребовать некоторого времени на привыкание к нему. Вообще синтаксис лямбда-выражений очень прямолинеен. Однако при встраивании его в код бывает не совсем легко расшифровать его и привыкнуть им пользоваться. Некоторые примеры функционального программирования с анонимными методами приводятся в главе 14.

Лямбда-выражения принимают две формы. Форма, которая наиболее прямо заменяет анонимные методы, представляет собой блок кода, заключенный в фигурные скобки. Я предпочитаю называть это лямбда-операторами. Такие лямбда-операторы — прямая замена анонимных методов. Лямбда-выражения, с другой стороны, предоставляют еще более сокращенный способ объявлять анонимный метод и не требуют ни кода в фигурных

Page 40: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

скобках, ни оператора return. Оба типа лямбда-выражений могут быть преобразованы в делегаты. Однако лямбда-выражения без блоков операторов представляют собой нечто действительно впечатляющее. Вы можете преобразовать их в деревья выражений с помощью типов из пространства имен System.Linq.Expressions. Другими словами, функция, описанная в коде, превращается в данные. Тему создания деревьев выражений из лямбда-выражений я раскрою ниже, в разделе "Деревья выражений" настоящей главы.

Лямбда-выражения

Для начала рассмотрим простейшую форму лямбда-выражений; те, что не содержат в себе блока операторов. Как упоминалось в предыдущем разделе, лямбда-выражение — это сокращенный способ объявления простого анонимного метода. Следующее лямбда-выражение может быть использовано в качестве делегата, принимающего один параметр и возвращающего результат выполнения операции над параметром: x => x / 2.

Это говорит следующее: "взять x в качестве параметра и вернуть результат следующей операции в x". Обратите внимание, что лямбда-выражение лишено информации о типе. Это не значит, что выражение не имеет типа. Вместо этого компилятор выводит тип аргумента и тип результата из контекста его использования, и отсюда следует, что если вы присваиваете лямбда-выражение делегату, типы определения делегата используются для определения типов внутри лямбда-выражения.

Когда лямбда-выражение присваивается делегату, тип возврата выражения обычно выводится из типов аргументов. Поэтому в следующем фрагменте кода тип возврата выражения – double, поскольку предполагаемый тип параметра x – double:

Func<double, int> expr = x => x / 2; // Ошибка компиляции!!!!

Исправить это можно, приведя результат тела лямбда-выражения к int:

Func<double, int> expr = x => (int) x / 2;

Явные типы в списке параметров лямбда-выражения необходимы, если делегат, которому вы его присваиваете, имеет out- или ref-параметры. Кто-то скажет, что явная фиксация типов параметров внутри лямбда-выражений лишает его элегантности и выразительной мощи. И это определенно затрудняет чтение кода. Теперь я хочу показать вам простое лямбда-выражение, не принимающее параметров:

public class LambdaTest{static void Main() {int counter = 0;WriteStream( () => counter++ );Console.WriteLine( "Финальное значение счетчика: {0}", counter );

}static void WriteStream( Func<int> counter ){for( int i = 0; i < 10; ++i )Console.Write( "{0}, ", counter() );

Console.WriteLine();}

}

Обратите внимание, насколько просто с использованием лямбда-выражения передать функцию в качестве параметра в метод WriteStream. Более того, переданная функция захватывает окружение, внутри которого она выполняется, а именно — значение counter в Main. В старые добрые времена C# 1.0 это было болезненным процессом и приходилось делать нечто вроде следующего:

unsafe public class MyClosure{int* counter;public MyClosure( int* counter ){this.counter = counter;

}public delegate int IncDelegate();public IncDelegate GetDelegate() { return new IncDelegate( IncrementFunction ); }int IncrementFunction() { return (*counter)++; }

}

Page 41: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

public class LambdaTest{ unsafe static void Main() {

int counter = 0;

MyClosure closure = new MyClosure( &counter );

WriteStream( closure.GetDelegate() ); Console.WriteLine( "Финальное значение счетчика: {0}", counter );}

static void WriteStream( MyClosure.IncDelegate incrementor ) {

for( int i = 0; i < 10; ++i )Console.Write( "{0}, ", incrementor() );

Console.WriteLine();}

}

Посмотрите, насколько больше работы требовалось выполнить, чтобы обойтись без лямбда-выражений. Я выделил дополнительный код и прочие изменения. Первая задача — создать объект для представления делегата и его окружения. В данном случае среда — это указатель на переменную counter в методе Main. Я решил использовать класс для инкапсуляции функции и ее окружения. Обратите внимание на использование небезопасного кода в классе MyClass для достижения этого. Затем в методе Main я создал экземпляр MyClosure и передал делегат, созданный вызовом GetDelegate методу WriteStream.

Какой объем работы! К тому же и понять такой код совсем нелегко.

В C# 2.0 появились анонимные методы, чтобы уменьшить это бремя. Однако они были не настолько функционально выразительны как лямбда-выражения, поскольку все еще придерживались старого императивного стиля программирования и требовали явного указания типов в списке параметров. Вдобавок синтаксис анонимных методов довольно громоздкий. Для наглядности следующий код демонстрирует, как можно было бы реализовать предыдущий пример на основе анонимных методов, чтобы вы увидели отличие в синтаксисе от лямбда-выражений.

public class LambdaTest{ static void Main() {

int counter = 0; WriteStream(delegate () { return counter++;} );

Console.WriteLine( "Финальное значение счетчика: {0}", counter );}

static void WriteStream( Func<int> counter ) {

for( int i = 0; i < 10; ++i )Console.Write( "{0}, ", counter() );

Console.WriteLine();}

}

Я выделил отличия между этим и исходным примером с лямбда-выражением. Определенно, он намного яснее, чем способ, которым приходилось пользоваться во времена C# 1.0. Однако он все еще не столь выразителен и краток, как версия с лямбда-выражением. И, наконец, в C# 3.0 мы получили элегантное средство определения потенциально очень сложных функций с применением лямбда-выражений, которые могут быть построены посредством сборки вместе других функций.

В предыдущем примере кода вы, вероятно, заметили последствия обращения к переменной counter внутри лямбда-выражения. В конце концов, counter – это локальная переменная внутри контекста Main, однако внутри контекста WriteStream на нее ссылаются при вызове делегата. В разделе “Остерегайтесь сюрпризов захваченных переменных” главы 10 я описал, как вы можете достичь того же результата с помощью анонимных методов. В терминологии функционального программирования это называется замыканием (closure). По сути, всякий раз, когда лямбда-выражение включает окружающую его среду, в результате получается такое замыкание. Как я покажу в следующем разделе, замыкания

Page 42: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

могут быть очень полезны. Однако, применяемые неправильно, замыкания чреваты неприятными сюрпризами. И, наконец, я хочу показать вам пример лямбда-выражения, принимающего более одного параметра:

public class LambdaTest{ static void Main() {

var teamMembers = new List<string> {"Lou Loomis","Smoke Porterhouse","Danny Noonan","Ty Webb" };FindByFirstName( teamMembers, "Danny", (x, y) => x.Contains(y) );

}static void FindByFirstName(List<string> members, string fName, Func<string,string,bool> predicate ) {foreach( var member in members ) {

if (predicate(member, fName))Console.WriteLine( member );

}}

}

В данном случае лямбда-выражение используется для создания делегата, принимающего два параметра типа string и возвращающего bool. Как вы можете видеть, лямбда-выражение представляет симпатичный и краткий способ создания предикатов. В разделе “Вернемся к итераторам и генераторам” далее в главе я представлю новую версию примера из главы 14, демонстрирующую использование лямбда-выражений в качестве предикатов для создания гибких итераторов.

Лямбда-операторы

Все лямбда-выражения, которые я продемонстрировал до сих пор, относились к типу простых выражений. Другой тип лямбда-выражений — те, которые я предпочитаю называть лямбда-операторами. По форме они подобны лямбда-выражениями из предыдущего раздела, но с тем отличием, что построены из составного блока операторов внутри фигурных скобок. По этой причине лямбда с блоками операторов должны иметь оператор return. Вообще все лямбда-выражения, показанные в предыдущем разделе, могут быть преобразованы в лямбда с блоком операторов, если их просто окружить фигурными скобками, предварив оператором return. Например, лямбда-выражение: (x, y) => x * y может быть переписано в виде блока операторов: (x, y) => { return x * y; }. В таком виде лямбда с блоками операторов почти идентичны анонимным методам. И есть одно главное отличие между лямбда с блоками операторов и простыми лямбда-выражениями. Первые могут быть преобразованы только в типы делегатов, в то время как вторые – и в делегаты, и в деревья выражений, посредством семейства типов, сосредоточенных вокруг System.Linq.Expressions.Expression<T>. О деревьях выражений мы поговорим в следующем разделе.

Большая разница между лямбда с блоками операторов и анонимными методами заключается в том, что анонимные методы должны явно указывать типы параметров, в то время как для лямбда компилятор почти всегда может выводить типы на основе контекста. Сокращенный синтаксис, представленный лямбда-выражениями, стимулирует в большей степени образ мышления и подходы функционального программирования.

Деревья выражений

До сих пор я показывал вам лямбда-выражения, подменяющие функциональность делегатов. Но, остановившись на этом, я оказал бы вам плохую услугу. Дело в том, что компилятор С# 3.0 также обладает способностью преобразовывать лямбда-выражения в деревья выражений на основе типов из пространства имен System.Linq.Expressions. Позднее, в разделе "Функции как данные", я объясню, чем они хороши. Например, вы уже видели, как можно конвертировать лямбда-выражение в делегат, как показано ниже:

Func<int, int> func1 = n => n+1;

В этой строке кода выражение преобразуется в делегат, принимающий единственный целочисленный параметр и возвращающий int. Однако взгляните на следующую модификацию:

Page 43: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

Expression<Func<int, int>> expr = n => n+1;

Вот это действительно круто! Лямбда-выражение, вместо того, чтобы конвертироваться в вызываемый делегат, преобразуется в структуру данных, представляющую операцию. Типом переменной expr является Expression<T>, где T заменяется типом делегата, в который может быть преобразовано лямбда-выражение. Компилятор замечает, что вы пытаетесь конвертировать лямбда-выражение в экземпляр Expression<Func<int,int>>, и генерирует весь необходимый код, чтобы это произошло. В некоторый момент позднее вы можете скомпилировать выражение в полезный делегат, как показано в следующем примере:

public class EntryPoint{ static void Main() {

Expression<Func<int, int>> expr = n => n+1; Func<int, int> func = expr.Compile();

for( int i = 0; i < 10; ++i ) Console.WriteLine( func(i) );}

}

Выражение компилируется в делегат. Если вы немножко задумаетесь, то сможете представить себе, как вы могли бы модифицировать это дерево выражений или даже комбинировать несколько деревьев выражений для создания более сложных деревьев выражений перед тем, как компилировать их. Можно даже определить новый язык выражений или реализовать анализатор для существующего языка выражений. Фактически, компилятор работает как анализатор выражений, когда вы присваиваете лямбда-выражение экземпляру типа Expression<T>. За кулисами он генерирует код для построения дерева выражений, и если вы используете ILDASM для просмотра сгенерированного кода, то увидите его в действии. Предыдущий пример может быть переписан без использования лямбда-выражений, как показано ниже:

public class EntryPoint{static void Main() {

var par = Expression.Parameter(typeof(int), "n");var expr = Expression<Func<int, int>>.Lambda<Func<int, int>>(Expression.Add(par, Expression.Constant(1)), par);

Func<int, int> func = expr.Compile();

for (int i = 0; i < 10; ++i)Debug.WriteLine(func(i));

}}

Три строки кода заменяют единственную строку предшествующего примера, где переменной expr присваивается лямбда-выражение n => n+1. Думаю, вы согласитесь, что первый пример читать намного легче. Однако этот длинный пример помогает выразить действительную гибкость деревьев выражений. Давайте разобьем на шаги процесс построения выражения. Сначала вы должны представить параметры в списке параметров лямбда-выражения. В данном случае параметр всего один – переменная n. Поэтому мы начинаем со следующего:

var par = Expression.Parameter( typeof(int), "n" );

В этих примерах я использую неявно типизированные переменные, чтобы сократить объем ввода и повысить читабельность кода. Напомню, что переменные строго типизированы. Компилятор просто выводит их тип во время компиляции вместо того, чтобы требовать от вас явного указания.

Эта строка кода говорит о том, что нам нужна переменная по имени par, относящаяся к типу int. Напомню, что в простом λ-выражении тип может быть определен на основе предоставленного типа делегата. Теперь нам нужно сконструировать экземпляр BinaryExpression, представляющий операцию сложения, как показано ниже:

Expression.Add(par, Expression.Constant(1))

Page 44: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

Здесь я говорю, что выражение BinaryExpression должно состоять из прибавления константы – числа 1 – к параметру par. Наверное, вы уже ухватили суть. Каркас реализует форму шаблона проектирования Abstract Factory (Абстрактная фабрика) для создания экземпляров элементов выражения. То есть, вы не можете создать новый экземпляр BinaryExpression или любого другого строительного блока деревьев выражений, и потому должны использовать статические методы класса Expression для создания этих экземпляров. Это дает нам, как потребителям, гибкость в выражении того, что мы хотим, и позволяет реализации Expression решать, какой тип нам действительно нужен.

Теперь, когда мы имеем BinaryExpression, мы должны использовать метод Expression.Lambda<> для привязки выражения (в данном случае par+1) с параметрами в списке параметров (в данном случае — n). Обратите внимание, что в примере я использую обобщенный метод Lambda<>, так что могу создать тип Expression<Func<int,int>>. Применение обобщенной формы предоставляет компилятору больше информации о типе, чтобы перехватывать любые ошибки, которые я мог бы внести, во время компиляции, не позволяя им нарушить работу приложения во время выполнения.

Если бы я использовал не обобщенную версию метода Expression.Lambda, то в результате получился бы экземпляр LambdaExpression. LambdaExpression также реализует метод Compile; однако вместо строго типизированного делегата он возвращает экземпляр типа Delegate. Прежде чем вы сможете вызвать экземпляр Delegate, вы должны привести его к определенному типу делегата, в данном случае Func<int, int>, или к другому делегату с той же сигнатурой, либо же вы должны будете вызвать DynamicInvoke на делегате. Любой из этих способов может привести к генерации исключения во время выполнения, если обнаружится несоответствие между вашим выражением и типом делегата, который, как вы думаете, оно должно генерировать.

Операции над выражениями

Теперь я хотел бы продемонстрировать пример того, как можно взять дерево выражений, сгенерированное из лямбда-выражения, и модифицировать его для создания нового дерева выражений. В данном случае я возьму выражение (n+1) и превращу его в 2*(n+1):

public class EntryPoint{ static void Main() {

Expression<Func<int,int>> expr = n => n+1;// Теперь присвоим expr значение исходного выражения, умноженное на 2.expr = Expression<Func<int,int>>.Lambda<Func<int,int>>(Expression.Multiply (expr.Body, Expression.Constant(2)), expr.Parameters);

Func<int, int> func = expr.Compile();for( int i = 0; i < 10; ++i )Console.WriteLine( func(i) );

}}

Очень важно отметить, что параметры, переданные методу Lambda<>, должны быть именно теми же экземплярами параметров, которые пришли из исходного выражения; т.е. expr.Parameters. Это обязательно. Вы не можете передать методу Lambda<> новый экземпляр ParameterExpression; в противном случае во время выполнения вы получите исключение, подобное описанному ниже, поскольку новый экземпляр ParameterExpression, даже имея то же имя, на самом деле является совершенно другим экземпляром параметра.

Существует много классов, унаследованных от класса Expression, и много статических методов создания его экземпляров и комбинации с другими выражениями. Я не стану здесь описывать их все. Поэтому я рекомендую обратиться к библиотеке документации MSDN, где содержатся все фантастические подробности о пространстве имен System.Linq.Expressions.

Функции как данные

Если вы когда-либо изучали функциональные языки вроде Lisp, то можете заметить сходство между деревьями выражений и тем, как Lisp и подобные ему языки представляют функции как структуры данных. Большинство людей сталкиваются с Lisp в академической среде, и часто изучаемые ими концепции оказываются неприменимыми в реальном мире.

Page 45: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

Но перед тем как вы решите, что деревья выражений — одно из таких академических упражнений, я хочу показать, насколько они могут быть полезны.

Как вы, возможно, можете предположить, внутри контекста C# 3.0 деревья выражений чрезвычайно полезны в применении к LINQ. Полное представление о LINQ я приведу в главе 16, а пока самый важный факт: LINQ представляет естественный для языка, выразительный синтаксис описания операций над данными, которые невозможно естественным образом смоделировать объектно-ориентированным способом. Например, вы можете создать выражение LINQ для поиска в большом массиве, находящемся в памяти (или другом типе IEnumerable), элементов, соответствующих определенному шаблону. LINQ – расширяемый язык и может предоставлять средства оперирования с другими типами хранилищ, такими как XML и реляционные базы данных. Фактически C# 3.0 представляет реализацию LINQ для реляционных баз данных (включая LINQ to SQL, LINQ to Dataset, LINQ to Entities, LINQ to XML и LINQ to Objects), которые все вместе позволяют выполнять операции LINQ на любых типах, поддерживающих IEnumerable.

Но как же деревья выражений действуют здесь? Предположим, что вы реализуете LINQ to SQL для запроса к реляционной базе данных. Пользовательская база может находиться на другом конце мира, и может оказаться слишком дорого выполнять простой запрос. К тому же вы не можете представить, насколько сложным может оказаться пользовательское выражение LINQ. Естественно, вы хотите сделать все, что можно для обеспечения максимальной эффективности.

Если выражение LINQ представлено в данных (как дерево выражений), а не в IL (как делегат), то вы можете оперировать с ним. Возможно, у вас есть алгоритм, который может выявлять места, где следует провести оптимизацию, тем самым упрощая выражение. Или, может быть, когда ваша реализация анализирует выражение, вы определяете, что все выражение может быть упаковано, отправлено по сети и полностью выполнено на стороне сервера.

Деревья выражений обеспечивают вам эту важную возможность. Затем, когда вы завершаете операции с данными, вы можете транслировать дерево выражений в окончательную исполняемую операцию посредством механизма типа LambdaExpression.Compile и запустить ее. Если бы выражение изначально было доступно только в виде кода IL, ваша гибкость была бы существенно ограничена. Я надеюсь, теперь вы можете оценить действительную мощь деревьев выражений в C# 3.0.

Полезные применения лямбда-выражений

Теперь, после того, как я продемонстрировал, как выглядят лямбда-выражения, давайте рассмотрим некоторые их применения. На самом деле вы можете реализовать большинство следующих примеров на C# 2.0, используя анонимные методы или делегаты. Однако поразительно то, насколько простое синтаксическое дополнение к языку может рассеять туман и открыть богатые возможности в плане выразительности.

Вернемся к итераторам и генераторам

В этой книге я уже пару раз описывал, как вы можете создавать пользовательские итераторы на C# 4. Теперь я хотел бы продемонстрировать, как можно использовать лямбда-выражения для создания пользовательских итераторов. То, что я хочу здесь подчеркнуть – способ реализации алгоритма в коде, в данном случае алгоритма итерации, который затем превращается в многократно используемый метод, который может применяться почти в любом сценарии.

В главе 9 итераторы представлены через оператор yield, а в разделе "Заимствование из функционального программирования" главы 14 рассматриваются пользовательские итераторы.

Те из вас, кто программировал на C++ и знаком с применением стандартной библиотеки шаблонов (STL), увидят в этой нотации нечто знакомое. Большинство алгоритмов, определенных в пространстве имен std в заголовочном файле <algorithm>, требуют для выполнения своей работы предоставления предикатов. Когда STL впервые появилась в начале 90-х годов, она захватила сообщество программистов C++ подобно свежему бризу функционального программирования.

Page 46: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

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

public static class Extender{public static IEnumerable<T> MyIterator<TColl, TCursor, T>(this TColl coll, TCursor cursor, Func<TColl, TCursor, T> GetCurrent, Func<TCursor, bool> IsOver, Func<TCursor, TCursor> Advance)

{while (!IsOver(cursor)){yield return GetCurrent(coll, cursor);cursor = Advance(cursor);

}}

}

Console.Write("\nMy Diagonal Iterator\n");var list2D = new List<List<double>> { new List<double> { 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9 },new List<double> { 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7 },new List<double> { 2.0, 2.1, 2.2, 2.3, 2.4 },new List<double> { 3.0, 3.1, 3.2, 3.3 }

};

var iter = list2D.MyIterator(new int[] { 0, 0 }, (a, c) => a[ c[0] ] [ c[1] ], c => c[0] > 3 || c[1] > 3, c => new int[] { c[0] + 1, c[1] + 1 });

foreach (var c in iter)Console.WriteLine("{0:f1}", c);

Смотрите, насколько многократно используемым является MyIterator<>! Общеизвестно, что для того, чтобы привыкнуть к применению лямбда-синтаксиса, нужно некоторое время, и те, кто привык к императивному стилю кодирования, могут столкнуться с определенными трудностями в его понимании. Обратите внимание, что он принимает три аргумента обобщенного типа. TColl — это тип коллекции, который в данном примере специфицирован в точке использования как List<List<double>>. TCursor – тип курсора, который в данном случае является простым массивом целых чисел, который может рассматриваться как координаты переменной list2D. T — это тип, возвращаемый кодом через оператор yield. Остальные параметры MyIterator<> — типы делегатов, которые он использует для определения того, как выполнять итерацию по коллекции. Для начала ему нужен способ получения доступа к текущему элементу коллекции, который выражается следующим лямбда-выражением:

(a, c) => a[ c[0] ] [ c[1] ]

Затем необходим способ определения факта достижения конца коллекции, для чего я применяю следующее лямбда-выражение:

(c) => c[0] > 3 || c[1] > 3

И, наконец, необходимо знать, как перемещать курсор, что я указываю в следующем лямбда-выражении:

(c) => new int[] { c[0] + 1, c[1] + 1 }

Другие реализации MyIterator<> могут принимать первый параметр типа IEnumerable<T>, который в данном примере должен быть IEnumerable<double>. Однако, когда вы накладываете это ограничение, то все, переданное MyIterator <>, должно реализовывать IEnumerable<>. Переменная list2D реализует IEnumerable<>, но не в той форме, которую легко использовать, поскольку это IEnumerable<List<double>>. Вдобавок вы можете предположить, что коллекция реализует индексатор, как описано в разделе "Индексаторы" главы 4, но тогда это наложит ограничение на повторную применяемость MyIterator<> и то, какие объекты вы можете использовать в нем. В примере, приведенном выше, индексатор в действительности используется для обращения к конкретному элементу, но

Page 47: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

его применение вынесено наружу и упаковано в лямбда-выражение, предназначенное для доступа к текущему элементу.

Более того, поскольку операции доступа к текущему элементу коллекции вынесены за скобки, вы можете даже трансформировать данные в исходной переменной list2D в процессе итерации по ней. Например, я мог бы умножить каждое значение на 2 в лямбда-выражении, которое обращается к текущему элементу коллекции, как показано ниже:

(a, c) => a[ c[0] ] [ c[1] ] * 2;

Можете вы представить, насколько мучительно было реализовать MyIterator<> с применением делегатов во времена C# 1.0? Именно это я имею в виду, говоря о том, что всего лишь добавление синтаксиса лямбда-выражений в C# 3.0 открывает глаза разработчику на невероятные возможности. Если вы запустите предыдущий пример, то увидите, что я прохожу матрицу по диагонали, что показывает следующий вывод:

0.01.12.23.3

В качестве финального примера рассмотрим случай, при котором ваш пользовательский итератор даже не выполняет итерации по элементам, а вместо этого использует генератор чисел, как показано ниже:

static IEnumerable<T> MyGenerator<T>(T init, Func<T, T> Advance){T current = init;while (true){yield return current;current = Advance(current);

}}

Код проверки статического метода MyGenerator.

Console.Write("\nMy Generator\n");var iter = MyGenerator<double>(1, x => x * 1.2);var en = iter.GetEnumerator();for (int i = 0; i < 10; ++i){en.MoveNext();Console.WriteLine(en.Current);

}

После запуска этого кода вы увидите следующий результат:

11.21.441.7282.07362.488322.9859843.58318084.299816965.159780352

Вы можете использовать этот метод для бесконечного выполнения, и тогда он остановится только по исключению переполнения памяти либо по принудительному останову. Но главное то, что элементов, по которым выполняется итерация, не существуют в коллекции; вместо этого они генерируются по мере необходимости при каждом перемещении итератора. Вы можете применить эту концепцию многими способами, даже создавая реализацию генератора случайных чисел через итераторы C#.

Замыкания (захват переменной) и мемоизация

In computer science, a closure is a first-class function with free variables that are bound in the lexical environment. Such a function is said to be "closed over" its free variables. A closure is de-fined within the scope of its free variables, and the extent of those variables is at least as long as the lifetime of the closure itself. The explicit use of closures is associated with functional program-ming and with languages such as ML and Lisp. Closures are used to implement continuation pass-

Page 48: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

ing style, and in this manner, hide state. Constructs such as objects and control structures can thus be implemented with closures. The concept of closures was developed in the 1960s and was first fully implemented as a language feature in the programming language Scheme. Since then, many languages have been designed to support closures. In some languages, a closure may occur when a function is defined within another function, and the inner function refers to local variables of the outer function.

В разделе "Остерегайтесь сюрпризов захваченных переменных" главы 10 я описал, как анонимные методы могут захватывать контекст своего лексического окружения. Многие называют этот феномен захватом переменной. На языке функционального программирования это также известно как замыкание (closure). Ниже показан простой пример замыкания в действии. Более развернутую дискуссию о замыканиях вы найдете по адресу http://en.wikipedia.org/wiki/Closure_%28computer_science%29.

Console.Write("\nClosure\n");int delta = 3;Func<int, int> f = x => x + delta;

for (int i = 0, n = 0; i < 10; ++i)Console.WriteLine(n = f(n));

Переменная delta и делегат f формируют замыкание. Тело выражения ссылается на delta и потому должно иметь доступ к ней при последующем выполнении. Чтобы обеспечить это, компилятор захватывает переменную для делегата. За кулисами же тело делегата содержит ссылку на действительную переменную delta. Более того, поскольку захваченная переменная доступна как делегату, так и контексту, содержащему лямбда-выражение, это означает, что захваченная переменная может быть изменена вне контекста и вне связи с делегатом. Такое поведение можно использовать в ваших интересах, но если его не ожидать, оно может вызвать серьезную путаницу.

В действительности при формировании замыкания компилятор C# 3.0 берет все эти переменные и упаковывает их в сгенерированный класс. Он также реализует делегат в качестве метода класса. В очень редких случаях вам может понадобиться учитывать это, особенно если обнаружится влияние на производительность при профилировании.

Теперь я хотел бы показать вам хорошее применение замыканий. Одной из основ функционального программирования является то, что сама функция трактуется как первоклассный объект, которым можно манипулировать и оперировать, а также вызывать. Вы уже видели, как лямбда-выражения могут быть преобразованы в деревья выражений, чтобы можно было оперировать ими, производя более или менее сложные выражения. Но один момент, о котором я еще не упомянул — это использование самих функций в качестве строительных блоков для создания новых функций. В качестве быстрой иллюстрации того, что я имею в виду, рассмотрим два лямбда-выражения:

x => x * 3x => x + 3.1415

Вы можете создать метод для комбинирования таких лямбда-выражений с целью создания составного лямбда-выражения, как показано ниже:

static Func<T, S> Chain<T,R,S>(Func<T,R> f1, Func<R,S> f2) { return x => f2(f1(x)); }

Console.Write("\n\nChain\n");Func<int, double> fid = Chain((int x) => x / 2, (int x) => x + Math.E);Console.WriteLine(fid (6));

Метод Chain<> принимает два делегата и производит третий делегат, комбинируя первые два. В методе Main вы можете видеть, как я использую его для производства составного выражения. Делегат, который вы получаете после вызова Chain<>, эквивалентен делегату, который вы получаете при преобразовании следующего лямбда-выражения в делегат:

x => x / 2 + Math.E

Метод вроде этого, способный связывать в цепочки произвольные выражения, действительно полезен, но давайте рассмотрим другие способы создания производных функций. Представьте себе операцию, которая требует действительно длительного времени на вычисление. Примерами могут служить операции вычисления факториала или операции вычисления n-го числа Фибоначчи. Пример, который я люблю демонстрировать – обратная константа Фибоначчи, которая выглядит так:

Page 49: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

Здесь Fk – k-е число Фибоначчи. (http://mathworld.wolfram.com/ReciprocalFibonacciConstant.html. Eric W. "Reciprocal Fibonacci Constant." MathWorld – A Wolfram Web Resource). Чтобы начать демонстрацию вычислений этой константы, сначала понадобится операция для вычисления n-го числа Фибоначчи:

Func<int, int> fib = x => x > 1 ? fib(x-1) + fib(x-2) : x;for(int i = 30; i < 40; ++i)Console.WriteLine( fib(i) );

Первое, что бросается в глаза при взгляде на этот код — формирование процедуры Фибоначчи, т.е. делегата fib. Он формирует замыкание на самом себе! Это определенно форма рекурсии, и она делает то, что надо. Однако если вы все-таки запустите этот пример, имея в своем распоряжении суперкомпьютер, то заметите, насколько медленно он работает, даже если все, что он делает – это вычисления с 30-го по 39-е число Фибоначчи! Если это так, нам даже не стоит надеяться продемонстрировать константу Фибоначчи. Такая медлительность обусловлена тем фактом, что каждое число Фибоначчи, которое мы вычисляем, требует немного больше работы, чем вычисление двух предыдущих чисел Фибоначчи, и в результате затрачиваемое время растет лавинообразно.

Эту проблему можно решить, пожертвовав пространством в пользу времени – за счет кэширования чисел Фибоначчи в памяти. Но вместо модификации исходного выражения давайте посмотрим, как можно создать метод, принимающий оригинальные делегаты в качестве параметра и возвращающий новый делегат, который заменяет оригинал. Конечная цель – получить возможность заменять первый делегат производным делегатом, не затрагивая код, который его использует.

Один из таких приемов называется мемоизацией (memoization). Это прием, посредством которого функция кэширования возвращает значения, и каждое возвращенное значение ассоциируется с входными параметрами.

In computing, memoization is an optimization technique used primarily to speed up computer pro-grams by having function calls avoid repeating the calculation of results for previously-processed inputs. Memoization has also been used in other contexts (and for purposes other than speed gains), such as in simple mutually-recursive descent parsing in a general top-down parsing algo-rithm that accommodates ambiguity and left recursion in polynomial time and space. Although re-lated to caching, memoization refers to a specific case of this optimization, distinguishing it from forms of caching such as buffering or page replacement. In the context of some Logic Program-ming languages, memoization is also known as tabling.

Это работает только в том случае, если функция не обладает энтропией – в том смысле, что для одних и тех же входных параметров всегда возвращает один и тот же результат. Тогда перед вызовом действительной функции вы сначала проверяете, не вычислялся ли результат для данного параметра ранее, и если да – возвращаете его вместо вызова функции. При очень сложных функциях такая техника требует немного больше пространства памяти, но дает существенный выигрыш в скорости. Подробнее о мемоизации вы можете прочесть по адресу http://en.wikipedia.org/wiki/Memoization. Вес Дайер (Wes Dyer) описывает мемоизацию в своем блоге http://blogs.msdn.com/wesdyer/archive/2007/01/ 26/function-memoization.aspx.

Рассмотрим пример. Поместите следующий extension-метод внутрь static-класса Extender.

public static Func<T, R> Memoize<T, R>(this Func<T, R> func){var cache = new Dictionary<T, R>();return (x) =>{R result = default(R);if (cache.TryGetValue(x, out result))

return result;result = func(x);cache[x] = result;

Page 50: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

return result;};

}

Код проверки extension-метода Memoize.

fib = x => x > 1 ? fib(x - 1) + fib(x - 2) : x;fib = fib.Memoize();Console.WriteLine("\nFibonacchi Memoization\n");for (int i = 30; i < 40; ++i)Console.WriteLine(fib(i));

Прежде всего, обратите внимание, что в Main добавился только один дополнительный оператор, в котором я применяю метод Memoize к делегату, чтобы произвести новый делегат. Все прочее остается без изменений, так что прозрачная взаимозаменяемость обеспечена. Метод Memoize упаковывает оригинальный делегат, который передан через аргумент func с другим замыканием, включающим экземпляр Dictionary<> для хранения кэшированных значений данного делегата func. В процессе Memoize взятие одного делегата и возврат другого реализует кэш, который значительно повышает эффективность. Всякий раз, когда вызывается делегат, он сначала проверяет, нет ли в кэше ранее вычисленного значения.

Конечно, мемоизация работает только с функциями, которые детерминировано воспроизводимы — в том смысле, что для одних и тех же параметров гарантируется один и тот же результат. Например, настоящий генератор случайных чисел невозможно подвергнуть мемоизации.

Запустите предыдущий пример и увидите поразительную разницу. Теперь мы вполне можем приступить к вычислению обратной константы Фибоначчи, модифицировав метод Main следующим образом:

Func<ulong, ulong> fib = null; fib = (x) => x > 1 ? fib(x-1) + fib(x-2) : x; fib = fib.Memoize(); Func<ulong, decimal> fibConstant = null;fibConstant = (x) => {

if( x == 1 )return 1 / (decimal)fib(x);

elsereturn 1 / ((decimal)fib(x)) + fibConstant(x-1);

}; fibConstant = fibConstant.Memoize();Console.WriteLine( "\n{0}\t{1}\t{2}\t{3}\n","Номер", "Фибоначчи".PadRight(24),"1/Фибоначчи ".PadRight(24), "Константа Фибоначчи".PadRight(24) );

for( ulong i = 1; i <= 93; ++i )Console.WriteLine("{0:D5}\t{1:D24}\t{2:F24}\t{3:F24}",i,fib(i),1/(decimal)fib(i),fibConstant(i));

Выделенный текст показывает делегат, который я создал для вычисления n-й обратной константы Фибоначчи. Вызывая этот делегат все с большим и большим значением x, вы должны заметить, что результат становится все ближе и ближе к обратной константе Фибоначчи. Обратите внимание, что я также осуществил мемоизацию делегата fibConstant. Если этого не делать, можно спровоцировать переполнение стека из-за рекурсии, по мере вызова fibConstant все с большими и большими значениями x. Так что вы можете убедиться, что мемоизация также экономит пространство стека за счет пространства кучи. В каждой строке вывода код показывает для информации промежуточное значение, но самое интересное значение – в крайней правой колонке. Обратите внимание, что я прекратил вычисление на итерации номер 93. Это потому, что ulong на 94-м числе Фибоначчи переполнится. Я мог бы решить эту проблему, используя BigInteger из пространства имен System.Numeric. Однако это не обязательно, поскольку 93-я итерация обратной константы Фибоначчи, показанная здесь, достаточно близка к цели этого примера.

3.359885666243177553039387

Я выделил значимые разряды. Думаю, вы согласитесь, что мемоизация чрезвычайно полезна. По этой причине еще много других полезных вещей можно сделать с методами, принимающими функции и производящими другие функции, что я и продемонстрирую в следующем разделе. Вы можете увидеть более точное значение обратной константы Фибоначчи по адресу http://www.research.att.com/~njas/sequences/A079586.

Page 51: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

Приправа

В предыдущем разделе, посвященном замыканиям, я продемонстрировал, как создать метод, который принимает функцию, переданную в виде делегата, и производит новую функцию. Это очень мощная концепция, и мемоизация, показанная в предыдущем разделе — пример отличного ее применения. В этом разделе я хотел бы продемонстрировать вам технику приправы (currying), которая по сути означает создание операции (обычно, метода), принимающей функцию с несколькими параметрами (обычно, делегат) и производящей только один параметр.

Подробнее об этом приеме читайте в http://en.wikipedia.org/wiki/Currying. Если вы – программист C++, знакомый с STL, то, несомненно, использовали операцию приправы, если имели дело с любой из привязок параметров вроде Bind1st и Bind2nd.

Предположим, что имеется лямбда-выражение, которое выглядит так: (x, y) => x + y. Теперь представьте, что у вас есть список действительных чисел двойной точности, и вы хотите использовать это лямбда-выражение для добавления константного значения к каждому элементу списка, тем самым производя новый список. Было бы здорово создать новый делегат на базе оригинального лямбда-выражения, где одна из переменных стала бы статическим значением. Это понятие называется привязкой параметра, и те, кто использовал STL в C++, знакомы с ним. Взгляните на следующий пример, где демонстрируется привязка параметров в действии:

public static class Extender{public static Func<T1, TResult> Bind2nd<T1, T2, TResult>(this Func<T1, T2, TResult> func, T2 constant ) { return x => func(x, constant); }

}

Следующий код добавьте в Main.

var mylist = new List<double> { 1.0, 3.4, 5.4, 6.54 };var newlist = new List<double>();Func<double, double, double> sum = (x, y) => x + y;var funcBound = sum.Bind2nd(3.2); // Здесь - приправленная функция

Console.WriteLine("\n\nOriginal List<double>");foreach (var item in mylist){Console.Write("{0}, ", item);newlist.Add(funcBound(item));

}Console.WriteLine("\n\nCurried List<double>");foreach (var item in newlist)Console.Write("{0}, ", item);

Мясо этого примера – в расширяющем методе Bind2nd<>, который я выделил. Вы можете видеть, что он создает замыкание и возвращает новый делегат, принимающий только один параметр. Затем, когда вызывается этот новый делегат, он получает только один параметр — первый параметр для исходного делегата, и передает ему константу в качестве второго параметра. Для примера я выполняю итерацию по списку myList, при этом строя новый список, содержащийся в переменной newList, используя приправленную версию оригинального метода, чтобы добавить 3.2 к каждому элементу.

Для сравнения я хочу показать и другой способ приправы, немного отличающийся от показанного в предыдущем примере:

public static class Extender{public static Func<T2, Func<T1, TResult>> Bind2nd<T1, T2, TResult>( this Func<T1, T2, TResult> func)

{return y => x => func(x, y);

}}

Следующий код добавьте в Main.

var mylist = new List<double> { 1.0, 3.4, 5.4, 6.54 };var newlist = new List<double>();

// Здесь - исходное выражение.

Func<double, double, double> func = (x, y) => x + y;var funcBound = func.Bind2nd()(3.2); // Здесь - "приправленная" функция.

Page 52: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

foreach(var item in mylist ){

Console.Write( "{0}, ", item ); newlist.Add( funcBound(item) );}

Console.WriteLine(); foreach(var item in newlist )

Console.Write( "{0}, ", item );

Я выделил части, отличающиеся от предыдущего примера. В первом примере Bind2nd<> возвращал делегат, принимающий один параметр и возвращающий целое число. В данном примере я изменил Bind2nd<> так, чтобы он принимал один параметр (значение, привязываемое ко второму параметру исходной функции) и возвращал другой делегат, приправляющий функцию. Обе формы совершенно корректны. Однако приверженцы чистоты стиля могут предпочесть вторую форму первой.

Анонимная рекурсия

В ранее приведенном разделе “Замыкание (захват переменной) и мемоизация” я показал форму рекурсии, использующую замыкания при вычислении чисел Фибоначчи. Для продолжения дискуссии давайте рассмотрим подобное замыкание, которое можно использовать для вычисления факториала числа:

Func<int, int> fact = null;fact = x => x > 1 ? x * fact(x-1) : 1;

Этот код работает, потому что fact формирует замыкание на самом себе и также вызывает себя. То есть вторая строка, где fact присваивается лямбда-выражение для вычисления факториала, захватывает сам делегат fact. Несмотря на то что такая рекурсия работает, она весьма хрупка, и нужно быть очень осторожным, применяя ее в таком виде.

Напомню, что невзирая на то, что замыкание захватывает переменную для использования внутри анонимного метода, который реализован здесь в виде лямбда-выражения, захваченная переменная остается доступной для изменения вне контекста захватывающего анонимного метода или лямбда-выражения. Например, рассмотрим, что произойдет, если мы поступим следующим образом:

Func<int, int> fact = null;fact = (x) => x > 1 ? x * fact(x-1) : 1;Func<int, int> newRefToFact = fact;

Поскольку объекты в CLR относятся к ссылочным типам, newRefToFact и fact теперь ссылаются на один и тот же делегат. Теперь предположим, что вы сделали нечто вроде следующего:

Func<int, int> fact = null;fact = x => x > 1 ? x * fact(x-1) : 1;Func<int, int> newRefToFact = fact;fact = x => x + 1;

Рекурсия разрушена! Заметили, почему? Причина в том, что мы модифицировали захваченную переменную fact. Мы присвоили ей ссылку на новый делегат, основанный на лямбда-выражении x => x+1. Но newRefToFact все еще ссылается на лямбда-выражение x => x > 1 ? x * fact(x-1) : 1. Однако, когда делегат, на который ссылается newRefToFact, вызывает fact, то он получает новое выражение x => x + 1, которое изменяет поведение имеющейся ранее рекурсии.

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

delegate TResult AnonRec<T, TResult>( AnonRec<T,TResult> f, T arg );AnonRec<int, int> fact = (f, x) => x > 1 ? x * f(f, x-1) : 1;

Суть в том, что вместо рекурсии, полагающейся на захваченную переменную, являющуюся делегатом, вы передаете делегат рекурсии в качестве параметра. В данном примере

Page 53: Секреты LINQ

Черносвитов Александр. Секреты LINQ. © 2007-2010

____________________________________________________________________________________________________________________________

делегат рекурсии представлен параметром f. Поэтому обратите внимание, что fact не только принимает f как параметр, но вызывает его для рекурсии и затем передает f следующей итерации делегата. По существу, захваченная переменная теперь находится в стеке, поскольку передается каждой рекурсии выражения. Однако, поскольку она в стеке, опасность модификации извне механизма рекурсии исключается. В http://en.wikipedia.org/wiki/Anonymous_recursion вы найдете теоретические сведения по анонимной рекурсии.

Чтобы подробнее ознакомиться с этой техникой, настоятельно рекомендую прочесть статью в блоге Веса Дайера (Wes Dyer) “Anonymous Recursion in C#” http://blogs.msdn.com/wesdyer. Вес Дайер – один из членов команды C# и ревностный сторонник функционального программирования. В этой статье он демонстрирует, как реализовать Y-комбинатор с фиксированной точкой, обобщающий понятие анонимной рекурсии, показанное ранее. Y-комбинаторы с фиксированной точкой описаны http://en.wikipedia.org/wiki/ Fixed_point_combinator.

Резюме

В этой главе я представил синтаксис лямбда-выражений, которые большей частью являются заменой анонимных методов. Фактически, очень жаль, что лямбда-выражения не появились в C# 2.0, потому что тогда не было бы необходимости в анонимных методах. Я продемонстрировал, как вы можете преобразовывать лямбда-выражения без тел операторов в делегаты. Вдобавок вы увидели, как лямбда-выражения без тел операторов конвертируются в деревья выражений на основе типа Expression<T>, определенного в пространстве имен System.Linq.Expression. Вы можете применять трансформации к деревьям выражений перед их компиляцией в делегат и вызовом. Я завершил главу демонстрацией полезных применений лямбда-выражений. К ним относится создание обобщенных итераторов, мемоизация с использованием замыканий, привязка параметров делегатов с помощью приправы, а также представление концепции анонимной рекурсии. Почти все эти концепции лежат в основе функционального программирования. Несмотря на то что все эти приемы можно было реализовать в C# на основе анонимных методов, добавление в язык лямбда-синтаксиса сделало их применение более естественным и менее сложным.

Следующая глава посвящена языку LINQ – кульминации всех новых средств C# 3.0. Также я продолжу уделять внимание связанным с ним аспектам функционального программирования.

One of the big benefits that the EDM lends to querying is that the relationships are built into the model and you won’t have to construct joins very often to access related data. Additionally, when using LINQ for the queries, the related data is presented via IntelliSense, which makes it very dis-coverable.