Сообщений 175    Оценка 1581 [+2/-0]         Оценить  
Система Orphus

LINQ как шаг к функциональному программированию

Автор: Чистяков Влад (VladD2)
The RSDN Group

Источник: RSDN Magazine #2-2008
Опубликовано: 26.01.2009
Исправлено: 10.12.2016
Версия текста: 2.0
Введение
Базис ФП – функция
Манипуляция функциями (ссылки на функции)
Тип функции
Делегаты
Эволюция анонимных методов в лямбда-выражения (или просто «лямбды»)
Зачем нужны лямбда-выражения?
ФП и SQL
Немного о проблемах делегатов
Кирпичики – или базовые «Функции высшего порядка» (ФВП)
Работа со списками
Объединяем все вместе
Вместо заключения

Введение

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

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

Цель этой статьи максимально просто объяснить императивному программисту основы функционального программирования.

Прошу прощения, если вас заденет словосочетание «императивный программист» или мои слова об этом гипотетическом создании. :) Как очень хорошо заметил Антон Злыгостев (Sinclair), «императивный программист» - это не должность, а стиль мышления. Все, кто изучал только императивное программирование (к нему относится и ООП), т.е. не изучал специально функциональное или логическое программирование, размышляя о программе или алгоритме, мыслят категориями «как сделать», а не «что сделать». Основная проблема освоения функционального программирования как раз и заключается в перестройке сознания на мышление категориями «что сделать» и, соответственно, восприятия кода, написанного в манере «что требуется получить», а также нескольких весьма простых приемов, позволяющих писать код более декларативно.

Совсем недавно (в 2006 году) я сам мыслил исключительно категориями «как сделать», и мои первые попытки изучить (или хотя бы понять) ФП не увенчались успехом. Со временем, разобравшись (по крайней мере, хочется верить в это :) ), я понял, что ничего сложного в ФП нет. Это всего лишь приемы программирования (очень похожие на паттерны проектирования) и некоторая поддержка со стороны языка программирования (и как следствие, его компилятора). Огромная проблема популяризаторов ФП заключается в том, что они не мыслят по-другому и, как следствие, разговаривают на другом языке, нежели те, кому они что-то пытаются объяснить. Вкупе с весьма специфичным (и непривычным для нас, императивных программистов) синтаксисом функциональных языков, изучение ФП превращается в весьма неприятный и трудный процесс самостоятельной ломки своего сознания. И все это без наркоза и соответствующего врачебного контроля! :)

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

Примеры в этой статье будут даваться в основном на C# 3.0, Microsoft .NET Framework 3.5. Совместимая с ним версия Microsoft VS – 2008. Лучше «брать» сразу с SP1.

ПРЕДУПРЕЖДЕНИЕ

В этой статье будут приводиться примеры, в основном связанные с «LINQ to object», так как именно LINQ to object является прямым аналогом функционального программирования. «LINQ to SQL» использует похожий код, но реально генерирует аналог абстрактного синтаксического дерева (эдакого CodeDom), по которому специализированными драйверами генерируется код SQL-запросов. Это выходит за рамки этой статьи, так что если вы встретите пример, реализацию некого метода или термин «LINQ», то знайте, что по умолчанию, речь идет о «LINQ to object».

Базис ФП – функция

Даже из названия «функциональное программирование» ясно, что основной упор в нем делается на функции. Функциональное вычисление – функция (или если быть точнее – «чистая функция», pure function) – должна принимать некоторые аргументы (входящие данные) на входе, производить вычисление и возвращать некоторый результат. При этом функция не должна создавать никаких побочных эффектов. Под побочными эффектами понимается:

  1. Изменять глобальные (статические в терминах C#) переменные или читать изменяемые данные.
  2. Вызывать другие функции которые могут создать побочный эффект или возвращать глобальные изменяемые данные.
  3. Заниматься любым вводом/выводом (да-да, не удивляйтесь).
  4. Посылать или принимать некие сообщения.

По сути, пункты 2-4 представляют собой разновидности одного и того же – изменение состояния или чтение изменяемого состояния посредством вызова.

В общем, функция не имеет право делать ничего, что могло бы изменить состояние чего бы то ни было, и полагаться на изменяемые (извне) данные. Все, что может сделать функция – это произвести вычисления и выдать результат.

У такого подхода есть одна замечательная особенность. Функция всегда должна возвращать один и тот же результат для одних и тех же аргументов. Замечательна эта особенность сразу по нескольким причинам.

  1. Легкость отладки: А. Такую функцию проще проверить тестами, так как она зависит только от параметров. Б. Все состояние (если его так можно назвать) при функциональных вычислениях располагается в стеке, так что его легко анализировать, и даже можно производить пошаговую отмену и повтор действий.
  2. Результаты работы функции можно кэшировать, что позволяет существенно повысить скорость работы некоторых алгоритмов.
  3. Легкость распараллеливания. Два вызова одной и той же функции совершенно независимы и могут выполняться параллельно. Кроме того, потенциально выполнение функции может быть автоматически распараллелено компилятором.
  4. Имеется потенциальная возможность доказать правильность функции. Данная область хорошо изучена математиками, и найдены формальные механизмы доказательства корректности функциональных программ. К сожалению, на практике такого ПО немного, и оно в основном носит исследовательский характер.
  5. Высокая повторная используемость функций. Независимость опять же играет на руку, и однажды написанную функцию легче использовать в другом месте программы (или в другой программе).

Фактически, отсутствие побочных эффектов – это заслуга не самой функции, а содержащихся в них выражений. Ведь именно в них требуется не допускать побочных эффектов. Меж тем, в императивном программировании (далее ИЯ) имеется множество конструкций создающих побочные эффекты. Главная из них, я бы даже сказал, основополагающая – это присваивание. Именно присваивание приводит к изменению памяти. Остальные конструкции, в основном, только способствуют или не способствуют применению присвоения. Так, среди способствующих конструкций можно перечислить:

Out- и ref-параметры.

Циклы.

Условные выражения, не позволяющие возвратить значения (а это все выражения C#, за исключением оператора «условие ? выражение1 : выражение2».

Способствует присвоениям и тот факт, что в C# нельзя возвратить из функции множество разнотипных значений, если только не поместить все их в некий объект (что приводит к множеству лишних действий и загромождению кода) или массив с элементами типа object (что совсем не здорово).

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

Есть языки, способные четко контролировать чистоту функций. Скажем, функциональный язык Haskell требует помещения императивных действий в специальные типы монад. Не спрашивайте у меня, что такое «монада». Это не могут внятно объяснить многие матерые haskell-программисты. :)

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

Манипуляция функциями (ссылки на функции)

Даже очень старые ИЯ, например, C и Паскаль, поддерживают самую базовую идею ФП – возможность манипуляции функциями. Однако в большинстве даже современных ИЯ реализована эта возможность совсем слабо. Обычно все ограничивается возможностью передать указатель на тело функции. Скажем, в С и C++ имя глобальной функции интерпретируется как указатель на нее.

ФЯ развивают эту идею, возводя ее в принцип – функция является в программе таким же полноценным объектом, как и экземпляр любого другого типа (например, экземпляр строки). Ей должно быть так же удобно манипулировать, как обычными объектами. Скажем, в С мы можем передать указатель на функцию другой функции, но составить (во время выполнения) из двух функций третью мы не в силах. Невозможно в С и объявить функцию по месту. В ФЯ же все это возможно. Манипулировать функциями в ФЯ очень просто и удобно.

Тип функции

Первое, что требуется для манипуляции функциями в статически типизированном языке – это иметь возможность описать тип функций. В ФЯ для функций был введен специальный тип – функциональный тип. Обычно он описывается как список типов параметров функции и список ее возвращаемых значений. Впрочем, тут возможны варианты, так как функция с несколькими параметрами может быть описана как функция с одним параметром, возвращающая функцию с другим параметром. Скажем, если для описания возвращаемого значения функции использовать знак «->», а для перечисления значений - «*», то функцию с сигнатурой:

        string Function(int x, string y);

Можно описать как:

        int * string -> string

или как:

        int -> string -> string

то есть как функцию, которая получает int и возвращает функцию, которая получает string и возвращает тоже string, то есть: int -> (string -> string). Я понимаю, что это очень непривычно и непонятно (на первый взгляд), но поверьте, что есть целая теория обосновывающая это («Теория лямбда-исчислений» Чёрча, http://ru.wikipedia.org/wiki/Лямбда-исчисление). Нам все эти тонкости нам сейчас не важны. Зато нам важно, как обстоят дела с аналогом функционального типа в C#.

Делегаты

В C# аналогом ссылки на функцию являются делегаты. В этом языке сама по себе функция (метод) или ее имя не являются чем-то, чем можно было бы манипулировать, но если поместить ее в делегат, появится возможность ссылаться на нее, а значит передавать в другие функции и возвращать функции из других функций. Таким образом, можно (с большой натяжкой) сказать, что делегаты являются аналогами функциональных типов в ФЯ. Однако они не являются полными аналогами, что создает некоторые проблемы при использовании делегатов в качестве функционального типа. Чуть позже я опишу проблемы делегатов, но сначала я расскажу о нововведении C# 3.0 – о лямбда-выражениях.

Эволюция анонимных методов в лямбда-выражения (или просто «лямбды»)

Очень важным механизмом манипуляции функциями являются возможность создания функций внутри методов, в том числе возможность создания анонимных функций. Такая возможность появилась в C# 2.0 под названием «анонимные методы». Зачем их было называть методами, и почему им сделали столь неуклюжий синтаксис, остается загадкой. Мне кажется, что неуклюжий синтаксис привел к тому, что программисты так и не стали использовать их повсеместно.

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

В C# 2.0 можно прямо посреди метода объявить анонимную функцию и присвоить ее переменной или передать другой функции:

        var myVariable = new Action(delegate() { некоторые действия });

Особую мощь анонимным методам придает тот факт, что в них доступны переменные, видимые в этом месте.

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

        var ary = newint[]{ 1, 2, 3, 4, 5 };

int sum = 0;
Array.ForEach(ary, delegate(int elem) { sum += elem; });

Console.WriteLine(sum);

Что выведет на консоль:

15

Это можно сделать потому, что анонимный метод «delegate(int elem) { sum += elem; }» имеет доступ к переменной sum, объявленной непосредственно перед ним. Такой «захват» переменных называется «лексическим замыканием» или просто «замыканием». Лексическим оно называется потому, что захват осуществляется в области лексической видимости.

ПРЕДУПРЕЖДЕНИЕ

Заметьте, что данный пример использует императивное изменение переменной sum. Это всего лишь пример, демонстрирующий замыкание, который легче понять начинающему. Ниже будет показано, как написать аналогичный код с использованием «чистых» функций (см. раздел «Свертка: Fold, FoldLeft, FoldRight, Reduce, Aggregate»).

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

        class Program
{
  static Action<int> ReturnClosure(intarg)
  {
    returndelegate(int x) { Console.WriteLine(arg + x); };
  }


  staticvoid Main()
  {
    var closure = ReturnClosure(6);
    closure(2);
    closure(3);
    closure(4);
  }
}

Данный код выводит на консоль:

8
9
10

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

В C# 3.0 появилось два важных изменения, затронувших анонимные методы. Точнее даже можно сказать, что анонимные методы были серьезно переработаны и поэтому названы лямбда-выражениями (Lambda expressions).

Откровенно говоря, лямбда-выражения – это просто доведенные до ума анонимные методы. Что же изменилось?

Мне кажется, что самым понятным объяснением того, что такое лямбда-выражения и с чем их едят, будет демонстрация трансформации двух анонимных методов в два же лямбда-выражения. По ходу дела я буду объяснять, что же, собственно, происходит. Вот как выглядят исходные анонимные методы:

        delegate(int arg1, string arg2) { return arg2 + " " + arg1; }
delegate(int arg1) { return arg1.ToString(); } 

Первое что изменилось – теперь перед анонимным методом больше не нужно писать ключевое слово delegate. Данное изменение очень приятно, так как ключевое слово «delegate» многих сбивало с толку, и поэтому очень часто можно было услышать, как анонимные методы называют «анонимными делегатами», хотя это в корне неверно.

Чтобы отличить лямбда-выражение, то есть анонимный метод, записанный в новом синтаксисе, от обычного выражения было решено после списка параметров указывать оператор «=>». Таким образом, мы можем изменить наш пример следующим образом:

(int arg1, string arg2) => { return arg2 + " " + arg1; }
(int arg1) => { return arg1.ToString(); }

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

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

(arg1, arg2) => { return arg2 + " " + arg1; }
arg1 => { return arg1.ToString(); }

Обратите внимание на то, что второе лямбда-выражение, кроме всего прочего, записано без скобок вокруг его единственного параметра. Для лямбда-выражения с одним параметром компилятор допускает обе формы записи (со скобками вокруг параметра и без них).

Последнее изменение заключается в том, что если внутри лямбда-выражения находится одно-единственное выражение (простите за тавтологию), то ключевое слово return и обрамляющие фигурные скобки можно опустить:

(arg1, arg2) => arg2 + " " + arg1
arg1 => arg1.ToString()

Собственно, именно поэтому термин «лямбда-выражение» и содержит в своем названии слово «выражение». Хотя если быть придирчивым, то не все лямбда-выражения содержат в себе выражения. :) Вы по-прежнему можете обрамить тело фигурными скобками, поместить в тело набор выражений (stаtеment-ов) и воспользоваться ключевым словом «return» (как это было показано выше). Кстати, в функциональном мире то, что называется лямбда-выражением, называется просто лямбдой. В ФЯ обычно вообще нет деления на expressions и statements (уж простите за английский, но в русском четкого разделения этих понятий нет; видимо, русский – исконно функциональный язык :). Поэтому нет и проблем с названиями.

Заметьте, как существенно сократилась запись! Было:

        delegate (int arg1, string arg2) { return arg2 + " " + arg1; }

стало:

(arg1, arg2) => arg2 + " " + arg1

А ведь семантически ничего не изменилось. Из «старых» анонимных методов всего лишь отжали воду.

При этом в Microsoft не придумали ровным счетом ничего нового. Примерно так выглядят лямбды почти во всех ФЯ. Так что совершенно непонятно, зачем было лепить в C# 2.0 этот безобразный, длинный и сбивающий с толку синтаксис. Понятно, что вывод типов был сделан только в C# 3.0, но, по крайней мере, все остальное-то точно можно было бы сделать сразу как в лямбда-выражениях!

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

Наверно, стоит чуть больше сказать о выводе типов. C# 3.0 поддерживает вывод типа из инициализации и по типу аргумента или переменной, в которые подставляется выражение. Таким образом, C# не может вывести тип просто для выражения «(arg1, arg2) => arg2 + " " + arg1». Однако если такое выражение подставить в качестве аргумента (чей тип известен компилятору) некоторого метода, то компилятор сможет вывести типы аргументов и возвращаемого значения. Так, следующий пример будет корректен:

        using System;

delegatestring Test(int arg1, string arg2);

class Program
{
  staticvoid Main(string[] args)
  {
    Test test = (arg1, arg2) => arg2 + " " + arg1;
    Console.WriteLine(test(123, "Test"));
  }
}

А следующий – нет:

        using System;

delegatestring Test(int arg1, string arg2);

class Program
{
  staticvoid Main(string[] args)
  {
    var test = (arg1, arg2) => arg2 + " " + arg1;
    Console.WriteLine(test(123, "Test"));
  }
}

Различия выделены красным.

Такой код:

        using System;

delegatestring Test(int arg1, string arg2);

class Program
{
  staticvoid PrintResult(Test test)
  {
    Console.WriteLine(test(123, "Test"));
  }

  staticvoid Main(string[] args)
  {
    PrintResult((arg1, arg2) => arg2 + " " + arg1);
  }
}

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

Зачем нужны лямбда-выражения?

Итак, мы разобрались, что же такое лямбда-выражения, но остался вопрос, зачем они нужны и при чем тут ФП?

Одним из самых эффективных приемов ФП (по сравнению с императивным программированием) является прием передачи небольшого, так сказать, уточняющего, выражения в некую универсальную функцию. Это позволяет вынести в стандартную библиотеку даже простейшие алгоритмы, которые в императивном программировании считаются неподдающимися декомпозиции и обычно описываются циклами. Это, в свою очередь, позволяет превратить невнятные, растянутые на несколько экранов, алгоритмы в удивительно краткие и в то же время выразительные выражения, очень похожие на запросы. Более того, понимая, что данная техника может оказаться плохо понятна для «интеллектуального большинства» (надеюсь, все понимают сарказм этой фразы...), в Microsoft решили придать ей внешний вид SQL-запросов. Расчет был очень прост – с SQL знакомы очень многие, а ФП в массы так и не проникло, несмотря на внушительный срок своего существования (ФП существует с середины 50-ых годов прошлого столетия). Как показывает практика, ставка оказалась вполне верной и большинство программистов нормально встретили LINQ.

ФП и SQL

Многим может показаться странным то, что ФП в LINQ замаскировано под SQL. Однако это не просто маскировка. Идеи ФП нацелены на то, чтобы сделать программирование более декларативным. Программируя в стиле ФП, мы как бы описываем то, что хотим получить, а не то, как мы это хотим получить. Именно это призван делать и SQL. SQL – это язык запросов, с помощью которого мы лишь описываем, что хотим выбрать из БД (или как хотим ее изменить), но при этом мы не задаем конкретный (императивный) алгоритм обработки данных.

Немного о проблемах делегатов

Как я уже говорил раньше, делегаты используются в C# в качестве функциональных типов. Однако при проектировании .NET-делегатов был сделан ряд ошибок, которые сказались при развитии C# в сторону ФП. Первая ошибка заключается в том, что по какому-то недоразумению делегаты являются не ссылками на функцию, а ссылками на одну или более функций, то есть делегат – это потенциально ссылка на массив функций, и это надо учитывать при проектировании собственного кода (и при реализации runtime-а .NET). Интересно, что реально в .NET существует Delegate, который подразумевает хранение ссылки только на один метод, и MulticastDelegate, который хранит ссылку на список методов, но спроектировано все так (а также реализовано в компиляторе C# и рантайме .NET), что фактически реальные делегаты всегда являются наследниками MulticastDelegate. Чтобы убедиться в этом, достаточно выполнить следующий код:

        using System;

class Program
{
  staticvoid Main()
  {
    Action del = new Action(Main);
    Console.WriteLine(del.GetType().BaseType.Name);
  }
}

Этот код выведет на консоль:

MulticastDelegate

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

Вторая ошибка дизайна создает куда больше проблем, нежели первая.

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

В свое время Хейльсберг (автор C#, в то время работавший в Borland над Delphi) написал открытое письмо в Sun Microsystem и Microsoft, в котором он говорил о том, что ему очень нравится Java, но кое что он хотел бы в ней поменять. Одним из этого «кое-чего» была возможность декларативного объявления событий в классах. Так как для метода-обработчика событий было важно, чтобы он мог принадлежать к другому типу и удерживать контекст экземпляра этого типа. Выражаясь проще, при генерации события должен быть вызван экземплярный метод-обработчик события, объявленный в другом классе. Казалось бы, это простое требование, но, как ни странно, в C++ нет прямого средства достичь этого. В Java, как ни странно, тоже нет подобной возможности. Вместо этого в ней предлагается реализовывать события вручную, храня ссылку на интерфейс, реализуемый объектом-обработчиком событий. Учитывая, что в Java в то время даже не было возможности объявить локальные классы, реализация событий и их обработчиков выливалась в приличный труд. Авторы Java отвечали на все претензии в духе: «Мы делали ООЯ, вот и используйте его».

В Microsoft же Хейльсберга поддержали и сделали главным архитектором нового языка, который традиционно был похож на Java, но другой (с) Рон Барк.

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

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

        delegate
        void MyDelegate1();

и

        delegate
        void MyDelegate2();

то присвоить им ссылку на один и тот же метод мы сможем:

MyDelegate1 del1 = new MyDelegate1(Main);
MyDelegate2 del2 = new MyDelegate2(Main);

а вот скопировать содержимое одного делегата в другой не сможем:

del1 = del2;

Причем проблема с копированием еще решаема, так как делегаты в C# имеют метод Invoke:

del2 = del1.Invoke;

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

        var myVariable = 1 + 2;

но не можем проделать то же самое с делегатом:

        var myVariable = Main;

Хотя этот код выглядит логично, и аналогичный код во всех без исключения функциональных языках работал бы, C# его не допускает, так как возникает неоднозначность – «какой из типов-делегатов выбрать?».

Таким образом, нам потребуется или явно создать экземпляр делегата:

        var myVariable = new Action(Main);

или объявить тип переменной:

Action myVariable = Main;

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

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

Такая проблема может возникнуть, например, если вам потребуется создать собственную реализацию EqualityComparer, основанную на лямбде:

        using System;
using System.Collections.Generic;
using System.Linq;

class FunctorEqualityComparer<T> : IEqualityComparer<T>
{
  privatereadonly Func<T, T, bool> _equalityComparer;
  privatereadonly Func<T, int> _getHashCode;

  public FunctorEqualityComparer(
    Func<T, T, bool> equalityComparer, 
    Func<T, int> getHashCode)
  {
    _equalityComparer = equalityComparer;
    _getHashCode = getHashCode;
  }

  public FunctorEqualityComparer(Func<T, T, bool> equalityComparer)
    : this(equalityComparer, null)
  {
  }

  publicbool Equals(T x, T y) { return _equalityComparer(x, y); }

  publicint GetHashCode(T obj)
  {
    return _getHashCode != null ? _getHashCode(obj) : obj.GetHashCode();
  }
}

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

        class Program
{
  staticvoid Main(string[] args)
  {
    var anonymousTypeInctance = new { A = 1, B = 2 };
    var equalityComparer = new FunctorEqualityComparer<что тут указать?>(
      (x, y) => x.A == y.A && x.B == y.B);
  }

Можно попытаться вынести код создания компаратора в отдельный метод (ведь для них действует вывод типов), но и это не пройдет, так как компилятор C# не сможет вывести тип.

ПРИМЕЧАНИЕ

К слову сказать, исходно функциональные языки (которые проектировались как ФЯ, а не адаптировались к ФП) поддерживают куда более продвинутые алгоритмы вывода типов, так что подобных проблем в них не возникнет.

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

        class Program
{
  staticvoid Main(string[] args)
  {
    varanonymousTypeInctance = new { A = 1, B = 2 };
    var equalityComparer = CreateEqualityComparer(
      anonymousTypeInctance, (x, y) => x.A == y.A && x.B == y.B);
  }

  privatestatic IEqualityComparer<T> CreateEqualityComparer<T>(
    T notUsed,
    Func<T, T, bool> equalityComparer)
  {
    returnnew FunctorEqualityComparer<T>(equalityComparer);
  }
}

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

Кирпичики – или базовые «Функции высшего порядка» (ФВП)

В ФП есть довольно краткий список функций высшего порядка (ФВП), которые входят во все библиотеки и массово применяются при написании повседневного кода.

ПРИМЕЧАНИЕ

Функция высшего порядка – это функция, которая получает другие функции в качестве параметров и/или возвращает функции в качестве возвращаемого значения.

Большинство этих функций теперь входят в LINQ (пространство имен System.Linq). Более того, LINQ расширяет их список так, чтобы код выражений, формируемых с их помощью, был похож на код SQL-запросов. Ниже, в разделе «стандартные ФВП», я перечислю каждую из этих функций, дам им объяснение и приведу пример, а пока же я объясню, что же такое ФВП.

Работа со списками

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

  1. Он допускает добавление элементов только в начало списка.
  2. Удаление элементов невозможно (но, как и любой другой объект в .NET, элементы освобождаются автоматически, если на них нет других ссылок).
  3. Для хранения каждого элемента создается отдельный объект.
  4. Доступ по индексу элемента возможен только перебором (т.е. имеет сложность O(n), в отличие, например, от O(1) в случае массива).

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

К сожалению, в .NET нет реализации однонаправленного связанного списка (класс LinkedList<Of> хотя и имеет созвучное название, но на деле является реализацией двунаправленного связанного списка, позволяющего изменять его содержимое по месту). Однако вы можете использовать сторонние реализации (их можно взять даже из библиотек ФЯ, например, в стандартной библиотеке языка Nemerle).

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

В C# 3.0 для выражения (и обработки) последовательностей используется тип IEnumerable<T>.

СОВЕТ

Новые функции обработки последовательностей в виде методов-расширений помещены в новую библиотеку System.Core.dll, которая поставляется вместе с .NET Framework 3.5. Вы можете увидеть все эти методы в классе Enumerable из пространства имен System.Linq. Однако C# 3.0 не прибит гвоздями ни к этому классу, ни к библиотекам, входящим в .NET Framework 3.5. Вместо этого C# 3.0 поддерживает так называемый Query-паттерн. Это означает, что для того чтобы воспользоваться новыми возможностями C# для поддержки встроенных в язык запросов (или просто функциями высшего порядка, описываемыми в данном разделе), вы можете просто вставить реализации необходимых функций в свой проект или подключить стороннюю библиотеку, реализующую их.

ПРЕДУПРЕЖДЕНИЕ

К сожалению, императивный программист в массе своей не привык думать об обработке последовательностей (списков) как о последовательности (ряде) преобразований, как это принято в ФП. Вместо этого императивный программист везде «видит» цикл. Вся обработка последовательности видится ему как содержимое этого цикла. В результате даже несложная обработка приводит к плохо читаемому коду, а уж сложная обработка последовательности превращается в настоящую кашу, которую и читать-то тяжело, а поиск ошибок в таком коде становится весьма нетривиальной задачей. ФП предлагает разбить обработку списка на несколько простых преобразований, которые можно выполнить последовательно. Так как на каждой стадии обработки получается (фактически) новая последовательность, отладка такого кода становится весьма простой задачей. Кроме того, читать такой код значительно проще (после некоторого периода привыкания).

Пожалуй, чаще всего используются следующие функции работы со списками: Fold – свертка (т.е. вычисление по списку некоторого значения), Map – отображение одного списка в другой с использованием функции преобразования элементов и Filter – фильтрация списка. С них и начнем, точнее с их аналогов.

Свертка: Fold, FoldLeft, FoldRight, Reduce, Aggregate

ПРЕДУПРЕЖДЕНИЕ

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

Функция свертки очень часто используется функциональщиками (людьми, подсевшими на ФП), и довольно редко – императивными программистами (хотя она и была доступна раньше в библиотеках вроде STL).

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

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

Пример – вычисление суммы массива:

          int[] array1 = { 1, 2, 3, 4, 5 };

Console.WriteLine(array1.Aggregate(0, (acc, elem) => acc + elem));

Этот код выведет на консоль:

15

То же самое можно сделать с помощью цикла:

          int[] array1 = { 1, 2, 3, 4, 5 };

int sum = 0;
for (int i = 0; i < array1.Length; i++)
  sum += array1[i];
Console.WriteLine(sum);
ПРИМЕЧАНИЕ

В LINQ для вычисления суммы последовательности (как, впрочем, и для вычисления минимального, максимального и среднего значения) есть специальная функция – Sum(). О ней речь пойдет позже. В данном случае нам интересно сравнение функционального императивного варианта (с целью облегчения понимания первого), а не сам пример.

В библиотеке «LINQ to object» Aggregate() реализован следующим образом:

          public
          static TAccumulate Aggregate<TSource, TAccumulate>(
  this IEnumerable<TSource> source, 
TAccumulate acc, 
  Func<TAccumulate, TSource, TAccumulate> func)
{
  if (source == null)
    throw New ArgumentNullException("source");
  if (func == null)
    throw New ArgumentNullException("func");

  foreach (TSource item in source)
    acc = func(acc, item);

  return acc;
}

Особенности:

Особенность 1: В ФЯ иногда можно встретить два варианта этой функции FoldLeft, FoldRight. Первая производит свертку от начала списка к концу, вторая, соответственно, наоборот, от конца к началу. Можно считать функцию FoldRight аналогом цикла с уменьшением индекса (обратного перебора). В .NET аналога FoldRight не существует. Радует только то, что FoldRight нужна не так уж часто.

Отсутствие аналога FoldRight, конечно же, факт неприятный, но особой проблемой это не является, так как, во-первых, FoldRight требуется довольно редко, а во-вторых, его нетрудно написать самостоятельно:

          public
          static TAccumulate AggregateRight<TSource, TAccumulate>(
  this IEnumerable<TSource> source, 
  TAccumulate acc, 
  Func<TAccumulate, TSource, TAccumulate> func)
{
  if (source == null)
    throw New ArgumentNullException("source");
  if (func == null)
    throw New ArgumentNullException("func");

  foreach (TSource item in source.Reverse())
    acc = func(acc, item);

  return acc;
}

publicstatic TAccumulate AggregateRight<TSource, TAccumulate>(
  this TSource[] source,
  TAccumulate acc,
  Func<TAccumulate, TSource, TAccumulate> func)
{
  if (source == null)
    throw New ArgumentNullException("source");
  if (func == null)
    throw New ArgumentNullException("func");

  for (int i = source.Length - 1; i >= 0; i--)
    acc = func(acc, source[i]);

  return acc;
}

Я реализовал специальную версию для массива, исходя из двух соображений:

  1. Массивы довольно часто используются там, где предъявляются особые требования к производительности.
  2. Копирование содержимого массива менее эффективно, чем доступ по индексу.

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

Вторая же реализация универсальна. Она использует Reverse из LINQ. Метод Reverse написан довольно хитро, что делает его весьма эффективным. Но об этом вы узнаете, когда дочитаете до описания этого метода. В принципе, можно было бы реализовать специализированную версию данного метода для IList<T>, но у меня есть большие подозрения, что от этого будет мало толку.

Особенность 2:

В LINQ реализован перегруженный вариант Aggregate, не принимающий начальное значение аккумулятора (который почему-то в Aggregate называется seed).

          public
          static TSource Aggregate<TSource>(
  this IEnumerable<TSource> source, 
  Func<TSource, TSource, TSource> func)
{
  if (source == null)
    thrownew ArgumentNullException("source");
  if (func == null)
    thrownew ArgumentNullException("func");

  using (IEnumerator<TSource> enumerator = source.GetEnumerator())
  {
    if (!enumerator.MoveNext())
      thrownew InvalidOperationException("No elements");

    TSource current = enumerator.Current;
    
    while (enumerator.MoveNext())
      current = func(current, enumerator.Current);

    return current;
  }
}

В качестве начального значения этот вариант метода Aggregate берет первое значение списка. При этом первый элемент исключается из списка обрабатываемых значений. Данный метода позволяет, например, упростить преобразование последовательности строк в строку разделенных запятой значений (о том, как преобразовать список объектов в список строк, будет рассказано в описании метода Select()):

          string[] array1 = { "1", "2", "3", "4" };

var result = array1.Aggregate((first, second) => first + ", " + second);
    
Console.WriteLine(result);

Этот код выводит на консоль:

1, 2, 3, 4

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

Ниже приведен аналогичный код, написанный в императивном стиле:

          string[] array1 = { "1", "2", "3", "4" };

string result = array1.Length == 0 ? "" : array1[0];

for (int i = 1; i < array1.Length; i++)
  result += ", " + array1[i];

Console.WriteLine(result);

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

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

          string[] array1 = { "1", "2", "3", "4" };

string result = null;

for (int i = 0; i < array1.Length; i++)
{
  result += array1[i];

  if (i != array1.Length - 1)
    result += ", ";
}

Console.WriteLine(result);

Этот код будет немного менее эффективен, так как проверка будет осуществляться на каждой итерации, но главное не это. Главное то, что императивный код может быть разным, хотя и делать одно и то же. В функциональном коде мы выражаем только требования (конкатенировать элементы в строку с разделителями), а значит написать код по-разному становится сложнее. Это делает функциональную запись более простой в чтении и кодировании. Например, когда я писал первый императивный вариант этого примера, то допустил ошибку, инициализировав переменную-счетчик «i» не единицей, а нулем. Это привело к тому, что на экран вывелось две единицы вместо одной. Допустить подобную ошибку в функциональном коде просто невозможно. Таким образом, выбирая функциональный стиль, мы лишаемся возможности сделать целый класс ошибок, что приводит к уменьшению общего числа ошибок, а значит, ускоряет процесс создания приложения.

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

Особенность 3:

В LINQ имеется вариант метода Aggregate(), принимающий, кроме прочего, функцию-селектор результирующего значения (resultSelector):

          public
          static TResult Aggregate<TSource, TAccumulate, TResult>(
  this IEnumerable<TSource> source, 
  TAccumulate seed, 
  Func<TAccumulate, TSource, TAccumulate> func, 
  Func<TAccumulate, TResult> resultSelector);

Функция resultSelector вызывается после обработки последовательности и позволяет преобразовать возвращаемое значение (в том числе и к другому типу). На мой взгляд, это не очень полезный вариант метода Aggregate, так как того же самого эффекта можно добиться последовательным вызовом обычного Aggregate() и Select() (о Select см. ниже). Возможно, такой вариант потребовался для реализации «LINQ to SQL», а возможно, просто кто-то перестарался.

СОВЕТ

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

Другие агрегатные функции

В ФЯ не всегда можно встретить функции типа Avg или Count, так как все они элементарно создаются на базе функции свертки. Однако LINQ проектировался в первую очередь как средство обработки финансовых данных, где такие функции используются постоянно. К тому же, они нужны для совместимости с SQL (в который преобразуются запросы «LINQ to SQL»). Поэтому в LINQ введены функции:

Для каждой из них создано множество перегруженных вариантов (для типов double, floar, decimal, int, long, их nullable-вариаций (т.е. double? и т.д.), а также варианты с функциями-селекторами). Описывать их здесь нет ни желания, ни возможности. К тому же ничего интересного в них нет, и как я уже говорил выше, все их можно реализовать через метод Aggregate. Просто важно знать, что они есть, чтобы, по случаю, не изобретать велосипед.

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

order.Details.Sum(d => d.UnitPrice * d.Quantity)

Отображение: Map, Convert, ConvertAll, Select

В функциональном программировании функция Map используется чрезвычайно часто. Она получила название «Map» потому, что в ФП ее рассматривают как функцию отображения одного списка в другой. Тип элементов исходного и результирующего списков могут различаться (но могут и совпадать).

В LINQ этой функции дали другое название – Select. Это было сделано для того, чтобы код «LINQ-запросов» был максимально похож на SQL:

          public
          static IEnumerable<TResult> Select<TSource, TResult>(
  this IEnumerable<TSource> source,
  Func<TSource, TResult> selector)
{
  if (source == null)
    thrownew ArgumentNullException("source");
  if (selector == null)
    thrownew ArgumentNullException("selector");

  foreach (TSource item in source)
    yieldreturn selector(item);
}

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

          int[] array1 = { 1, 2, 3, 4 };

var result = array1.Select(elem => elem.ToString());

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

Этот код выводит на консоль:

1
2
3
4

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

          int[] array1 = { 1, 2, 3, 4 };

var result = new List<string>();

for (int i = 0; i < array1.Length; i++)
  result.Add(array1[i].ToString());

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

Если объединить этот пример с примером к методу Aggregate, то можно получить весьма компактный и выразительный вариант вывода списка в виде строки элементов, разделенных запятой:

          int[] array1 = { 1, 2, 3, 4 };

var result = array1.Select(elem => elem.ToString())
                   .Aggregate((first, second) => first + ", " + second);

Console.WriteLine(result);

Этот кода выведет на консоль:

1, 2, 3, 4

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

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

Способ 1:

          int[] array1 = { 1, 2, 3, 4 };

string result = null;

for (int i = 0; i < array1.Length; i++)
{
  if (i != 0)
    result += ", ";

  result += array1[i].ToString();
}

Console.WriteLine(result);

Именно так обычно пишет код 90% императивных программистов. Причем чем больше нужно выполнить действий над элементами списка, тем больше и сложнее становится тело цикла (а так же его пролог и эпилог). Немудрено, что в большинстве случаев такой код становится нечитаемым.

ПРЕДУПРЕЖДЕНИЕ

Если вы сразу захотели покритиковать данный код, например, объяснить автору, что настоящие императивные программисты напишут этот код с использованием StringBuilder, или просто захотите предложить немного другую реализацию, то остановитесь и прочитайте следующие абзацы! Ведь вы снова мыслите императивно, то есть о том «как сделать?», а не о том, «что делать?».

Способ 2:

          int[] array1 = { 1, 2, 3, 4 };

var result1 = new List<string>();

for (int i = 0; i < array1.Length; i++)
  result1.Add(array1[i].ToString());

string result2 = result1[0];

for (int i = 1; i < array1.Length; i++)
  result2 += ", " + result1[i];

Console.WriteLine(result2);

Понятное дело, что «так» императивные программисты не пишут. Не делают они это по двум причинам.

Во-первых, из соображений производительности (хотя в 99% случаев она вряд ли пострадает). Ведь создается промежуточная последовательность (в данном случае динамический массив типа List<string>()), а это не оптимально!

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

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

Обратите внимание на то, что ФВП, обрабатывающие строки (в функциональном примере), прекрасно объединяются в последовательности (применяются последовательно). Это позволяет очень компактно записывать весьма сложные преобразования.

Кроме того, обратите внимание на то, что этот код можно читать почти как спецификацию:

          // преобразовать исходную последовательность в последовательность строк
.Select(elem => elem.ToString()) 
// преобразовать результат предыдущей операции в строку с разделителями
.Aggregate((first, second) => first + ", " + second); 
// и так далее...

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

СОВЕТ

Если вам поначалу трудно читать код с лямбда-выражениями, то попытайтесь себе представить, что тело лямбды находится внутри цикла, а имя ФВП определяет эпилог и пролог этого цикла (то есть задает паттерн обработки). Другими словами смотрите на применение ФВП как на некий макрос (сокращение кода) который раскрывается в цикл, а содержимое лямбда-выражений подставляются в тело этого цикла. Параметры лямбды, при этом, являются интерфейсом между ней и телом подразумеваемого цикла.

Это сильно упростит восприятие функционального кода на начальном этапе. Данное мысленное преобразование требуется потому, что императивный программист за долгие годы привык думать низкоуровневыми понятиями (паттернами кодирования) циклов и простейших операций. ФП же предлагает думать более крупными операциями, такими как отображение, фильтрация и свертка/агрегация.

А теперь можно сказать пару слов о «StringBuilder», то есть поговорить об эффективности. Парадокс заключается в том, что чем на более высоком уровне абстракции выражается мысль, тем проще создать эффективную реализацию для этой мысли (задачи). Для нашего примера самым эффективным (и при этом в высшей степени декларативным) решением будет создать функцию Join, объединяющую элементы последовательности в строку с разделителями. Вот она уже может быть реализована как угодно.

Хорошим подходом будет вводить такие функции по мере появления в них нужды. Заведите себе модуль (статический класс в терминах C#) Utils и складывайте туда подобные мелкие функции-помощники. При этом, чтобы не тратить на их реализацию много времени, их поначалу можно реализовывать в функциональном стиле. А потом, если потребуется, их код можно будет переписать более эффективно в императивном стиле. Например, с использованием все тех же цикла и StringBuilder-а.

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

Особенность 1:

Во многих ФЯ есть вариант функции отображения, передающей в функцию преобразования (селектор в .NET), полученную в качестве параметра, не только элемент последовательности, но и индекс переданного элемента. Такая функция может иметь как то же значение, так и другое (например, MapI()). В .NET она имеет то же имя – Select. Вот ее сигнатура:

          public
          static IEnumerable<TResult> Select<TSource, TResult>(
  this IEnumerable<TSource> source, 
  Func<TSource, int, TResult> selector)

Таким образом, мы можем использовать знание об индексе элемента при преобразовании его значения. Следующий код преобразует список строк в нумерованный (с единицы) список:

          string[] array1 = { "Иванов", "Петров", "Сидоров", "Пупкин" };

var result = array1.Select((elem, i) => (i + 1) + ". " + elem + "\.n")
                   .Aggregate((first, second) => first + second);

Console.WriteLine(result);

Данный код выводит на консоль:

1. Иванов.
2. Петров.
3. Сидоров.
4. Пупкин.

Фильтрация: Filter, Where

Filter тоже является основополагающей операцией обработки списков и используется в функциональном коде очень часто. Суть операции Filter ясна из его названия – это фильтрация элементов списка на основе предиката (булевой функции). Методу Filter() в качестве параметра передается функция-предикат, которая применяется к элементам последовательности, и если она возвращает true, то этот элемент попадает в результирующую последовательность. Вот как реализован метод Where:

          public
          static IEnumerable<TSource> Where<TSource>(
  this IEnumerable<TSource> source, 
  Func<TSource, bool> predicate)
{
  if (source == null)
    thrownew ArgumentNullException("source");
  if (predicate == null)
    thrownew ArgumentNullException("predicate");

  foreach (TSource item in source)
    if (predicate(item))
      yield return item;
}
ПРИМЕЧАНИЕ

Чтобы в дальнейшем не забивать статью повторяющимся кодом печати последовательностей (а их будет еще немало), в дальнейшем я буду применять для их печати следующие функции:

          static
          void PrintSeq<T>(IEnumerable<T> seq, string separator)
{
  var result = seq.Select(elem => elem.ToString())
                  .Aggregate((first, second) => first + separator + second);
  Console.WriteLine(result);
}

staticvoid PrintSeq<T>(IEnumerable<T> seq) { PrintSeq(seq, ", "); }
ПРИМЕЧАНИЕ

Первая позволяет вывести последовательность с заданным разделителем, а вторая является сокращением первой и выводит последовательность с разделителем «запятая».

Следующий пример выбирает из списка четные значения (отвечающие предикату «значение % 2 == 0»):

          int[] array1 = { 1, 2, 3, 4 };

var result = array1.Where(elem => elem % 2 == 0);

PrintSeq(result);

Этот код выведет на консоль:

2, 4

Тот же код может быть записан в императивном стиле следующим образом:

          int[] array1 = { 1, 2, 3, 4 };

var result = new List<int>();

foreach (int elem in array1)
  if (elem % 2 == 0)
    result.Add(elem);

PrintSeq(result);

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

Особенность 1:

В LINQ имеется перегруженный вариант метода Where, который, как и аналогичный вариант метода Select, передает предикату кроме значения элемента еще и его индекс:

          public
          static IEnumerable<TSource> Where<TSource>(
  this IEnumerable<TSource> source, 
  Func<TSource, int, bool> predicate)

Он может применяться в тех случаях, когда в логике обработки участвует индекс элемента.

Следующий пример выбирает «каждого второго» (т.е. имена, идущие в списке с четными индексами):

          string[] array1 = { "Иванов", "Петров", "Сидоров", "Пупкин" };

var result = array1.Where((name, i) => i % 2 == 0);

PrintSeq(result);

Этот код выводит на консоль:

Иванов, Сидоров

Разворот списка: Rev, Reverse

Разворот списка очень часто используется в ФЯ, но это, скорее, вызвано не тем, что такая операция действительно так уж часто встречается в работе, а тем, что для представления последовательности в ФЯ обычно применяют однонаправленный связанный список. Одной из его особенностей является то, что если при обработке одного списка генерируется другой, то список получается развернутым в обратную сторону. Для многих алгоритмов это не важно, но если это важно, то приходится применять данную функцию. Для LINQ это не важно, так как вместо однонаправленного связанного списка в LINQ применяется чистая абстракция – интерфейс System.IEnumerable<T>, а реализация LINQ-функций в основном построена на итераторах (специальном синтаксисе, основанном на ключевом слове yield, см. http://msdn.microsoft.com/en-us/library/9k7k7cf0(VS.80).aspx и/или раздел «Итераторы» статьи «Нововведения в C# 2.0»).

В LINQ Reverse нужен хотя бы потому, что в нем нет аналога FoldRight (свертки справа налево), а есть только аналог FoldLeft – Aggregate. Применением Reverse перед Aggregate можно добиться функциональности, аналогичной FoldRight:

seq. Reverse().Aggregate(...)

Следующий пример выводит элементы списка в обратном порядке:

          string[] array1 = { "Иванов", "Петров", "Сидоров", "Пупкин" };

PrintSeq(array1.Reverse());

Этот код выведет на консоль:

Пупкин, Сидоров, Петров, Иванов

Надеюсь, не надо объяснять, что, скажем, применение Where, принимающего предикат с индексом после применения Reverse, будет генерировать индексы уже для развернутой последовательности? Таким образом, код:

          string[] array1 = { "Иванов", "Петров", "Сидоров", "Пупкин" };

PrintSeq(array1.Reverse().Where((name, i) => i % 2 == 0));

Выведет на консоль «Пупкин, Петров», а не «Иванов, Сидоров», как в исходном примере с Where.

Сортировка: Sort, (в LINQ: OrderBy, OrderByDescending, ThenBy и ThenByDescending)

Сортировка, по сравнению с приведенными выше функциями, довольно редко применяется в функциональных программах. Пожалуй, в LINQ она будет применяться несколько чаще (хотя все равно, скорее всего, тоже не очень часто). Дело в том, что LINQ, скорее всего, будет применяться в приложениях, связанных с обработкой данных на предприятиях, а бухгалтеры, менеджеры и прочие финансисты очень любят, когда списки и таблички красиво отсортированы (и имеют строку «Итого:») :). ФП обычно применялось для вычислительных и исследовательских задач, где расчеты были просто несопоставимы с форматированием результата.

Видимо поэтому функции сортировки из стандартных библиотек ФЯ отличаются от функций сортировки LINQ.

Отличие состоит в том, что принимают эти функции. В ФЯ функции сортировки обычно принимают (кроме сортируемого списка) функцию-предикат, позволяющую определить, является ли один элемент списка большим другого, или предикат позволяющий сравнить два элемента и возвращающий 1 (если первый элемент больше), 2 (если меньше) и 0 (если элементы равны). Это, кстати сказать, принято также и в императивных языках (в том числе в C# в, так сказать, доLINQовскую эру).

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

К сожалению, передать информацию о направлении сортировки в функцию сортировки не просто. Поэтому в LINQ пошли другим путем. Создали четыре функции: OrderBy, OrderByDescending, ThenBy и ThenByDescending. Первые две используются для начальной сортировки, а вторые две – если нужно отсортировать функцию (внутри уже отсортированных групп) еще по одному критерию.

Чтобы продемонстрировать данный подход, мне придется использовать более сложные структуры данных, нежели встроенные типы int и string. Я воспользовался новым типом данных, появившимся в C# 3.0 – анонимными типами. Вкратце, анонимный тип – это класс без имени, объявляемый по месту инициализации его экземпляров. Типы данных его свойств (а полей они иметь не могут) выводятся из инициализирующих значений (которые обязаны быть). Имена свойств определяются прямо по месту. В общем, смотрите спецификацию или читайте статьи на RSDN, посвященные C# 3.0 и LINQ. Думаю, в данных примерах и так все будет очевидно. Единственное, что стоит заметить – это то, что в начале следующего примера объявляется массив анонимных типов, имеющих два поля: Name типа string и Age типа int.

Этот пример выводит список объектов анонимного типа, отсортированный сначала по имени (полю Name), а затем по возрасту (полю Age):

          var array1 = new[] 
{ 
  new { Name="Иванов",  Age=23 },
  new { Name="Петров",  Age=21 },
  new { Name="Сидоров", Age=40 },
  new { Name="Иванов",  Age=70 },
  new { Name="Пупкин",  Age=54 },
};

PrintSeq(array1.OrderBy(c => c.Name).ThenByDescending(c => c.Age), "\n");

Думаю, что пример очевиднее, чем объяснение. :)

Если вам необходимо отсортировать последовательность по нескольким критериям (ключам), то необходимо последовательно вызвать метод ThenBy и/или ThenByDescending.

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

          public
          interface IOrderedEnumerable<TElement> : IEnumerable<TElement>, IEnumerable
{
  IOrderedEnumerable<TElement> CreateOrderedEnumerable<TKey>(
    Func<TElement, TKey> keySelector, 
    IComparer<TKey> comparer, 
    bool descending);
}

Реализация этого интерфейса не выполняет немедленной сортировки. Вместо этого она откладывает сортировку до момента, когда будет вызван метод GetEnumerator (унаследованный от интерфейса IEnumerable<T>). Интерфейс же IOrderedEnumerable<TElement> позволяет, через свой метод CreateOrderedEnumerable, «уточнить» параметры сортировки.

В общем, «не верь глазам своим!». Последовательность «.OrderBy(c => c.Name).ThenByDescending(c => c.Age)» не означает приказа «немедленно отсортировать значение по полю Name, а потом то, что получится, отсортировать еще и по полю Age». Она означает «создать объект, производящий сортировку полю Name, а потом уточнить этот объект, создав объект, который сортирует последовательность по полю Name, и внутри групп с одинаковым именем уже по убыванию по полю Age». Конечно, и при таком подходе есть непроизводительные затраты. Ведь для каждого ключа требуется создать свой объект сортировки. Но все же, количество критериев сортировки обычно не очень велико, и производительность получаемого объекта сортировки не зависит от количества ключей сортировки. А это намного важнее.

Особенность 1:

Внимательный читатель должен заметить, что у метода CreateOrderedEnumerable имеется параметр comparer типа IComparer<TKey>. Он позволяет задать объект-компаратор для конкретного ключа. Чтобы этой возможностью можно было воспользоваться, все четыре метода (OrderBy, OrderByDescending, ThenBy и ThenByDescending) имеют перегруженную версию, принимающую в качестве параметра не только ключ, но и объект-компаратор. Вот полный список методов сортировки из типа Enumerable:

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

publicstatic IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(
  this IEnumerable<TSource> source, 
Func<TSource, TKey> keySelector, 
IComparer<TKey> comparer);

publicstatic IOrderedEnumerable<TSource> OrderByDescending<TSource, TKey>(
  this IEnumerable<TSource> source, 
Func<TSource, TKey> keySelector);

publicstatic IOrderedEnumerable<TSource> OrderByDescending<TSource, TKey>(
  this IEnumerable<TSource> source, 
Func<TSource, TKey> keySelector, 
IComparer<TKey> comparer);

publicstatic IOrderedEnumerable<TSource> ThenBy<TSource, TKey>(
  this IOrderedEnumerable<TSource> source, 
Func<TSource, TKey> keySelector);

publicstatic IOrderedEnumerable<TSource> ThenBy<TSource, TKey>(
  this IOrderedEnumerable<TSource> source, 
Func<TSource, TKey> keySelector, 
  IComparer<TKey> comparer);

publicstatic IOrderedEnumerable<TSource> ThenByDescending<TSource, TKey>(
  this IOrderedEnumerable<TSource> source, 
Func<TSource, TKey> keySelector);

publicstatic IOrderedEnumerable<TSource> ThenByDescending<TSource, TKey>(
  this IOrderedEnumerable<TSource> source, 
Func<TSource, TKey> keySelector, 
  IComparer<TKey> comparer);

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

Собственно, никто не мешает создать собственные методы сортировки, принимающие в качестве параметра просто предикат, сравнивающий два элемента. Проблема только в том, что собственные методы не будут работать в случае, скажем, «LINQ to SQL», так как драйвер просто не будет знать, как переписать их в SQL.

Пожалуй, в данном месте первый раз стоит сказать о синтаксисе, применяемом в C# 3.0 для написания запросов. Дело в том, что компилятор C# 3.0 позволяет вместо использования ФВП (описанных выше и дальше) использовать специальный синтаксис, очень похожий на SQL. Думаю, что об этом вы и сами уже давно знаете. Так вот, до сих пор этот синтаксис практически один к одному перекладывался на вызов методов. Единственная разница заключалась в том, что вместо описаний параметров лямбд в нем один раз на запрос описывается имя, скажем так, для отдельной записи. Делается это так:

          from СИНОНИМ_ДЛЯ_ОТДЕЛЬНОЙ ЗАПИСИ in ИМЯ_ПОСЛЕДОВАТЕЛЬНОСТИ

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

          where УСЛОВИЕ_ФИЛЬРАЦИИ
select ОПИСАНИЕ_ВОЗВРАЩАЕМОГО_ЗНАЧЕНИЯ

Но с сортировкой все немного иначе. Вместо множества последовательных вызовов имеет место запись:

          orderby СПИСОК_ПОЛЕЙ_ПЕРЕЧИСЛЕННЫХ_ЧЕРЕЗ_ЗАПЯТУЮ 

Если для некоторого поля нужно указать, что оно должно сортироваться в обратном порядке, то после него ставится ключевое слово «descending». В общем, как в SQL. Компилятор же переписывает этот синтаксис в набор последовательных вызовов, точно так, как если бы мы писали их вручную. Таким образом, приведенный выше пример сортировки по двум полям в синтаксисе LINQ будет выглядеть следующим образом:

          var array1 = new[] 
{ 
  new { Name="Иванов",  Age=23 },
  new { Name="Петров",  Age=21 },
  new { Name="Сидоров", Age=40 },
  new { Name="Иванов",  Age=70 },
  new { Name="Пупкин",  Age=54 },
};

PrintSeq(from c in array1 orderby c.Name, c.Age descending select c, "\n");

Кстати, используя расширение синтаксиса, в конце запроса обязательно нужно писать ключевое слово select. Как говорилось в одном анекдоте – «Объясныт это нэлза! Это можьно толко запомныт!!!».

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

ПРЕДУПРЕЖДЕНИЕ

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

Спрямление: Flatten, SelectMany

Flatten в функциональных языках несколько отличается от того, что понимается под SelectMany. SelectMany получает список списков, а на выходе дает один список, содержащий элементы всех вложенных списков. Однако суть этих методов похожа – они позволяют превратить набор вложенных последовательностей в одну «плоскую».

SelectMany отличается тем, что ему передается не последовательность, которую нужно «выпрямить», а функция-селектор, которая должна возвратить последовательность для каждого элемента исходной последовательности. На выходе же она возвращает «выпрямленную» последовательность, содержащую все элементы последовательностей, возвращенных функцией-селектором.

Чтобы было понятнее, приведу пример работы этой функции. Сначала опишу задачу. Представьте, что у нас есть набор (массив) фраз типа (скажем, строк в файле):

          "The quick brown", 
"fox jumped over", 
"the lazy dog."

Нам нужно преобразовать их в последовательность слов. Вот как это будет выглядеть без использования SelectMany (т.е. в полуимперативной манере):

          var sentence = new [] 
  {
    "The quick brown", 
    "fox jumped over", 
    "the lazy dog."
  };

// Отображаем массив строк в последовательность массивов строк.
IEnumerable<string[]> segments = sentence.Select(w => w.Split(' '));

var result = new List<string>();
// Перебираем последовательность массивов...foreach (string[] segment in segments)
  // добавляем набор строк (сегмент) к результирующей последовательности.
  result.AddRange(segment);

PrintSeq(result);

Этот код выведет на консоль:

The, quick, brown, fox, jumped, over, the, lazy, dog.

А вот как тот же код будет выглядеть с применением функции SelectMany:

          var sentence = new [] 
  {
    "The quick brown", 
    "fox jumped over", 
    "the lazy dog."
  };

var result = sentence.SelectMany(segment => segment.Split(' '));

PrintSeq(result);

И то же самое с использованием расширенного синтаксиса:

          var sentence = new [] 
  {
    "The quick brown", 
    "fox jumped over", 
    "the lazy dog."
  };

var result = 
    from segment in sentence
    from word in segment.Split(' ')
    select word;

PrintSeq(result);

Группирование: Group, GroupBy

Как и метод упорядочивания, метод группирования значений отличается от распространенных в ФЯ вариантов тем, что получает на входе не предикат-компаратор, а функцию-селектор ключа, по которому будет производиться группирование значений. При этом метод GroupBy возвращает последовательность, состоящую из ссылок на реализацию интерфейса IGrouping<TKey, TElement>, описывающих отдельную группу:

          public
          interface IGrouping<TKey, TElement> 
: IEnumerable<TElement>, IEnumerable
{
  TKey Key { get; }
}

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

Кроме того, есть реализации метода GroupBy, принимающие в качестве параметра функцию-селектор результата (resultSelector) и/или функцию-селектор элементов (elementSelector).

Ниже приведен весь список перегрузок данного метода (модификаторы public static опущены для краткости):

IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>(
  this IEnumerable<TSource> source, 
  Func<TSource, TKey> keySelector);

IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>(
  this IEnumerable<TSource> source, 
Func<TSource, TKey> keySelector, 
  IEqualityComparer<TKey> comparer);

IEnumerable<IGrouping<TKey, TElement>> GroupBy<TSource, TKey, TElement>(
  this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, 
Func<TSource, TElement> elementSelector);

IEnumerable<TResult> GroupBy<TSource, TKey, TResult>(
  this IEnumerable<TSource> source, 
Func<TSource, TKey> keySelector, 
  Func<TKey, IEnumerable<TSource>, TResult> resultSelector);

IEnumerable<IGrouping<TKey, TElement>> GroupBy<TSource, TKey, TElement>(
  this IEnumerable<TSource> source, 
Func<TSource, TKey> keySelector, 
  Func<TSource, TElement> elementSelector, 
  IEqualityComparer<TKey> comparer);

IEnumerable<TResult> GroupBy<TSource, TKey, TElement, TResult>(
  this IEnumerable<TSource> source, 
Func<TSource, TKey> keySelector, 
  Func<TSource, TElement> elementSelector, 
  Func<TKey, IEnumerable<TElement>, TResult> resultSelector);

IEnumerable<TResult> GroupBy<TSource, TKey, TResult>(
  this IEnumerable<TSource> source, 
Func<TSource, TKey> keySelector, 
  Func<TKey, IEnumerable<TSource>, TResult> resultSelector, 
  IEqualityComparer<TKey> comparer);

IEnumerable<TResult> GroupBy<TSource, TKey, TElement, TResult>(
  this IEnumerable<TSource> source, 
Func<TSource, TKey> keySelector, 
  Func<TSource, TElement> elementSelector, 
  Func<TKey, IEnumerable<TElement>, TResult> resultSelector,
  IEqualityComparer<TKey> comparer);

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

Для демонстрации работы метода GroupBy придется вернуться к нашим (то ли братьям, то ли однофамильцам) Иванову, Петрову, Сидорову и Пупкину. Назовем их «персонами». Следующий пример группирует список персон по фамилиям, вводит имя каждой группы (т.е. фамилию), количество персон с этой фамилией и саму информацию о каждой персоне:

          var array1 = new[] 
  { 
    new { Name="Иванов",  Age=23 },
    new { Name="Петров",  Age=21 },
    new { Name="Сидоров", Age=40 },
    new { Name="Иванов",  Age=70 },
    new { Name="Пупкин",  Age=54 },
    new { Name="Иванов",  Age=37 },
    new { Name="Пупкин",  Age=94 }
  };

var groups = array1.GroupBy(cust => cust.Name).OrderBy(grp => grp.Key);

foreach (var group in groups)
{
  Console.WriteLine("Группа '{0}'. Количество элементов в группе: {1}",
    group.Key, group.Count());

  foreach (var item in group)
    Console.WriteLine("\t" + item);
}

Этот код выведет в консоль:

Группа 'Иванов'. Количество элементов в группе: 3
        { Name = Иванов, Age = 23 }
        { Name = Иванов, Age = 70 }
        { Name = Иванов, Age = 37 }
Группа 'Петров'. Количество элементов в группе: 1
        { Name = Петров, Age = 21 }
Группа 'Пупкин'. Количество элементов в группе: 2
        { Name = Пупкин, Age = 54 }
        { Name = Пупкин, Age = 94 }
Группа 'Сидоров'. Количество элементов в группе: 1
        { Name = Сидоров, Age = 40 }

Объединяем все вместе

Мне кажется, как-то скучно все время оперировать простенькими примерами, совершенно оторванными от реальной жизни. Так что напоследок продемонстрирую пример, найденный мной как-то в Интернете, и немного доработанный напильником.

Этот пример выводит все типы, сгруппированные по пространствам имен и отсортированные по пространствам имен же, а внутри групп еще и по именам типов, из всех модулей сборки, в которой находится тип System.Linq.Enumerable. Фух! Даже описать это на словах непросто. Как же должен выглядеть код, добывающий такую информацию? Совсем не сложно!.. если вы используете функциональный подход, LINQ и C# 3.0:

        var assembly = Assembly.GetAssembly(typeof(System.Linq.Enumerable));

Console.WriteLine("Assembly: " + assembly.GetName().Name);

var groups = assembly.GetModules()
  .SelectMany(module => module.GetTypes().OrderBy(type => type.Name))
  .GroupBy(type => type.Namespace)
  .OrderBy(group => group.Key);
foreach (var group in groups)
{
  Console.WriteLine(group.Key);

  foreach (Type type in group)
    Console.WriteLine("\t" + type.Name);
}

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

...
System.Collections.Generic
        BitHelper
        ElementCount
        Enumerator
        HashHelpers
        HashSet`1
        HashSetDebugView`1
        HashSetEqualityComparer`1
        Slot
System.Diagnostics
        EventSchemaTraceListener
        TraceLogRetentionOption
        TraceWriter
        UnescapedXmlDiagnosticData
...

На всякий случай я отделил мишуру (вроде получения сборки, вывода информации о ней и вывода текста на консоль), пометив код запроса красным.

А кто-то ведь сетовал на то, что работать с рефлексией сложно... :)

Вместо заключения

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

Если вы просто хотите освоить LINQ и его синтаксис, зайдите на эту страницу.


Эта статья опубликована в журнале RSDN Magazine #2-2008. Информацию о журнале можно найти здесь
    Сообщений 175    Оценка 1581 [+2/-0]         Оценить