C#, asp net.ajax разработка web-приложений, Javascript CSS
 
Задать вопрос asp.net ajax C#

Рубрики


Статьи


Подписка


Подписаться по RSS

Архив

 Полный архив по категориям

Популярные записи


Мои рекомендации



# Wednesday, April 18, 2007

Методы расширения (Extension Methods)

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

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

public static class Util

{

    public static int ElementCount(IEnumerable enumerable)

    {

        int i = 0;

        foreach (var e in enumerable)

            i++;

        return i;

    }

}

 

// И тогда, для любого массива

//

IEnumerable<int> array = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

 

// Можно было бы посчитать количество элементов

//

Console.WriteLine(Util.ElementCount(array)); // 10

Но тут есть пара мелких неудобств. Во-первых, для того чтобы использовать класс Util и его методы, надо знать что он есть, по массиву-то ни как не вычислишь, что существует Util с таким замечательным методом. А во-вторых, если нам понадобится передать в утилитный метод какие-нибудь параметры, да еще совершить несколько операций подряд, то синтаксис этого дела будет довольно страшненьким. Вто время, как для запросов к коллекциям это довольно типичная ситуация и запутаться где класс над которым производится операция, где утилитный метод, а где параметры будет очень легко. И выход придумали следующий – достаточно изменить одну строчку кода в утилитном классе:

// добавить this перед первым параметром метода

//

public static int ElementCount(this IEnumerable enumerable)

// и вызывать этот утилитный метод можно так

//

int count = array.ElementCount();

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

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

При этом добавление this не отменяет старого способа обращения к утилитному методу, его вполне можно вызвать как раньше: int count = Util.ElementCount(array);

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

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

И так, картина потихоньку начинает вырисовываться... Теперь у нас есть возможность строить примерно такие выражения:

// псевдокод

//

var result = CollectionA.Join(CollectionB, <условие бьединения>).GroupBy(<условие группировки>).Where(<условие фильтрации>).Select(<описание нового типа>);

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

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

§  Функция может быть определена в любом месте.

§  Функцию можно передать в качестве параметра в другую функцию.

§  Функцию можно вернуть в качестве параметра из другой функции.

Если вспомнить C# версии 1.0, то в тот момент он не поддерживал подобного рода конструкции. Пытливому уму должно быть очевидно, что делегаты в принципе подходят на роль функций первого класса, но в первой версии C# их нельзя было обьявить в произвольном месте. Скажем, внутри метода сделать этого не удалось бы..

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

// для начала опишем ExtensionMethod для фильтрации

//

public static IEnumerable Filter<T>(

    this IEnumerable<T> enumerable,

    ConditionDelegate<T> condition)

{

    foreach (T e in enumerable)

        if (condition(e))

            yield return e;

}

 

// Естественно, где-то должен быть описан и ConditionDelegate

//

public delegate bool ConditionDelegate<T>(T condition);

 

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

// сначала где-то реализовать тело делегата

//

public static bool GetEven(int n)

{

    return (n % 2 == 1) ? false : true;

}

 

// Ну, а потом произвести непосредлственно фильтрацию передав делегат

// в качестве параметра

//

foreach (var e in array.Filter(GetEven))

                Console.Write(e + ", ");

 

В некоторых случаях это даже удобно, если нам надо одно и тоже условие подпихнуть в несколько разных мест, но на практике такие делегаты используются по большей части только один раз и объявлять их намного удобнее «по месту». По этой причине, так как такая ситуация встречается не только при работе с коллекциями, уже во второй версии C# ввели конструкцию по имени «анонимный метод». Анонимный метод уже полностью удовлетворяет определению первоклассной функции, так как может быть определен в любом месте, в том числе и в теле метода. Таким образом, вышеприведенный код фильтрации с использованием анонимных методов, может трансформироваться в следующее:

foreach (var e in array.Filter(

    delegate(int n)

    {

        return (n % 2 == 1) ? false : true;

    }))

Console.Write(e + ", ");

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

Обратите так же внимание, что в здесь мне не пришлось указывать тип параметра делегата при обращении к утилитному методу – Filter(…), а не Filter<int>(…). Здесь нам опять пришел на помощ вывод типов.

Так, конечно, удобно описывать делегат в месте использования, но кратким подобный синтаксис не назовешь, и можно представить, какой длинны может получиться тщательно выпеленный запрос... Я не зря проводил аналогию с функциональными языками, там с этой проблемой столкнулист гораздо раньше и с успехом побороли путем сублимации синтаксиса, назвав такую конструкцию лямбда-функциями.  Вот и в третьей версии шарпа решили выкинуть все лишнее и сократить запись анонимного метода до необходимого минимума. Лишним оказалось, слово «delegate», указание типа параметра (опять спасибо вычислению типов), фигурные скобки и слово «return», а для отделения параметра от тела метода ввели пару символов =>. И на выходе получили лямбда-выражение (Lambda Expression), которые опять таки, давно и с удовольствием используются в функциональных языках.

С применением вышеописанного синтаксиса вызов нашего метода фильтрации будет выглядеть так:

foreach (var e in array.Filter(n => n % 2 != 1))

                Console.Write(e + ", ");

 

 

// То есть, объявление делегата

//

delegate(int n)

  {

        return (n % 2 == 1) ? false : true;

  }

 

//Трансформировалось в

//

n => n % 2 != 1

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

ConditionDelegate<int> IsEven = n => n % 2 != 1;

Console.WriteLine(IsEven(2));

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

Скажем, подсчитать количество четных элементов в вышеприведенном массиве можно будет примерно так:

var q = array.Where(n=>n%2==0).Count();

 

// или так:

//

var q = array.Count(n=>n%2==0);

 

Но на самом деле, это только пол-дела, продолжние, пожалуй, последует...

Используем LINQ. Часть 1
Разработка с использованием LINQ. Часть 2

По материалам http://blogs.gotdotnet.ru/personal/bezzus/

Comments are closed.