Вы находитесь на странице: 1из 51

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

Черносвитов Александр. Секреты LINQ. © 2007-2010 ____________________________________________________________________________________________________________________________ Секреты LINQ Язык С# в отличие от C++ не

Секреты 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-ю методами, которые являются особыми новыми конструкциями, называемыми extension methods (расширения). Все расширения уже реализованы классом Enumerable и о них не надо беспокоиться. Приведем краткое описание некоторых из этих методов:

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

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>, который является производным от IEnumerable<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

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)

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

partial (method)

remove

select

set

value

var

where (generic type constraint)

where (query clause)

yield

 

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

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

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

int? i = null;

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

string

s = 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; }

Ранее такая запись допускалась только при объявлении интерфейса. Инициализаторы объектов и коллекций (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 нет ни одного конструктора. Автоматически сгенерированный конструктор без параметров пользуется автоматически реализуемыми свойствами.

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

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 {

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

}

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

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

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

class Stud

{

 

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

}

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

При этом отпадает необходимость объявлять скрытые переменные. Раскрывать код аксессоров 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[] {

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, декларируют делегатные типы (не делегаты). Они лишь задают сигнатуры методов (правила их использования), которые могут быть вызваны с помощью делегата. Говорят, что они задают сигнатуры возможных заданий делегатов. Сигнатурой метода называются правила его вызова, а именно: тип возвращаемого им значения и список типов его параметров.

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

В пространстве имен 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"); 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 InvocationList

  • 1. Method = Say, CallingConvention = Standard, HasThis, Target = Test.TestDelegate, Returns = Void

  • 2. 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> определяет тип указателей на функции с

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

сигнатурой 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 ("%SystemDrive%");.

Следующая версия шаблона 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-делегата типа Action<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 параметров.

Семейство 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,997494986604054 pFunc = 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).

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

В пространстве имен 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>(

...

);

создает новый делегатный тип (ge-

neric delegate), который описывает любые функции, имеющие 5 входных параметров (a, b, c, d, e) и

возвращающие значение типа res (не void — для void-методов используйте шаблон Action<>).

Итак, мы создали новый шаблон делегатных типов. После этого следует определить множество параметров шаблона, которое дает заказ компилятору на создание нового делегатного типа. Так, выбрав множество <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() {

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

Console.WriteLine (Half (3)); // 1,5 Console.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 отсутствует имя метода (поэтому делегат и называется анонимным). Кроме того, тело внешней функции перекочевало внутрь метода 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()

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

{

 

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, реализованный в виде λ-выражения, производит вывод переменной 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

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

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. Она сообщает, что типом возвращаемого значения будет IEnumerable<string>. Таким образом, это выражение можно заменить на эквивалентное ей, но не использующее описатель var (анонимного типа).

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

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

c => c == "Where"

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

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

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

Рекурсивное определение: IEnumerable<T> описывает последовательность объектов типа T, а последовательность обозначает все коллекции, работающие по правилам IEnumerable<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 нельзя заменить чем-либо другим.

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

Отладчик сообщит нам, что тип объекта 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);

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

{ 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 — вы получите ошибку.

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

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

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

static IEnumerable<int> Powers (int n) {

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

}

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

Console.WriteLine ("\n\nPowers\n"); foreach (var v in Powers(10)) Console.Write (v + ", ");

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

Generic-классы

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

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

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

Параметр 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)

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

{

 

if (node == null) node = new Node<T>(item); else if (item.CompareTo(node.data) < 0) Add(item, ref node.left); else Add(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) {

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);

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

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> мы получаем эту коллекцию и выводим ее содержимое простым перечислением, без позиционирования на экране узлов дерева.

В классе 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, каким,

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

например, является класс String. Подобный прием давно существует в языке JScript. Теперь, благодаря усилиям команды Anders Hejlsberg'а (создателя языка C#), такая возможность появилась и в C#.

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

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 additional 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

Console.WriteLine(text.TrimAll());

} static void Main() {

extra

spaces

";

Console.BackgroundColor = ConsoleColor.Blue; Console.ForegroundColor = ConsoleColor.Yellow; Console.Clear();

TestTrimAll();

}

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

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

Рассмотрим пример, который расширяет класс 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));

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

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

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

public static class Dumper {

// ======= Первый аргумент extension-метода должен иметь описатель this public 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, трактуемый как свойство (property), и обычное свойство Length. Поэтому мы увидим следующий результат.

Chars

--> unknown

Length

-->

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 Charles

Song --> 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.

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

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

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

string s = null; 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).

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

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

При обработке запроса компилятор создает объекты типа <Anonymous Type>. Вы можете увидеть этот тип в окне отладчика Autos при пошаговом выполнении кода. Найдите в этом окне, какой тип компилятор вывел (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 }; // Ряд чисел Fibonacci Predicate<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

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

4

Объединение (Join)

Join, GroupJoin

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, LastOrDefault, 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>. Так как метод Contains возвращает 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" }; 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);

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

Делегатный тип вычисляется на основе анализа λ-выражения: (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

1

five

4

2

six

3

3

seven

5

4

eight

5

5

nine

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. Такой подход к реализации exten- sion-методов повышает эффективность цепочки вычислений, за счет экономии памяти, отводимой на хранение промежуточных результатов. Рассмотрм пример, в котором используется цепочка вызовов 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; foreach (var s in q) Console.WriteLine("{0}. {1}, {2,-2}, {3}", ++k, s.Date.ToShortDateString(), s.len, s.Song);

Отметим, что компилятор распознает контекстное ключевое слово yield только если после него следует оператор return или