Анонимные методы в C# 2.0

Автор: Patrick Smacchia
Перевод: Козлов Руслан
Источник: C#2 Anonymous Methods
Материал предоставил: RSDN Magazine #2-2006
Опубликовано: 30.07.2006
Версия текста: 1.0
Введение в анонимные методы C# 2.0
Анонимные методы могут принимать аргументы
Синтаксические тонкости
Анонимные методы и generic-и
Анонимные методы в реальном мире
Компилятор C# 2.0 и анонимные методы
Простой путь
Захват локальных переменных
Захват локальных переменных и сложность кода
Доступ анонимного метода к аргументу внешнего метода
Доступ из анонимного метода к члену внешнего класса
Анонимные методы и замыкания
Определения: замыкание и лексическое окружение
Прогулка по замыканиям
Использование замыканий вместо классов
Делегаты и замыкания
Анонимные методы и функторы
Введение в функторы
Использование анонимных методов и функторов для запросов к коллекциям
Поддержка функторов в классах List и Array

Код к статье

Статья представляет новое свойство языка C# версии 2.0, называемое анонимными методами. В отличие от дженериков анонимные методы не используют новые инструкции на уровне Intermediate Language. Вся магия осуществляется в компиляторе.

ПРИМЕЧАНИЕ

Статья написана на основе книги автора «Practical .NET2 and C# 2.0». Сайт книги: http://www.practicaldot.net/

Введение в анонимные методы C# 2.0

Давайте начнем с изменения кода C#1 на код C# 2.0 с использованием анонимных методов. Вот простая программа на C#1, в которой сначала получается ссылка, а затем по этой ссылке через делегат вызывается метод:

Example1.cs
class Program 
{
  delegate void DelegateType();

  static DelegateType GetMethod()
  {
    return new DelegateType(MethodBody);
  }

  static void MethodBody()
  {
    System.Console.WriteLine("Hello");
  }

  static void Main()
  {
    DelegateType delegateInstance = GetMethod();
    delegateInstance();
    delegateInstance();
  }
}

Вот та же самая программа, переписанная с использованием анонимных методов C# 2:

Example2.cs
class Program
{
  delegate void DelegateType();

  static DelegateType GetMethod()
  {
    return delegate() { System.Console.WriteLine("Hello"); };
  }

  static void Main()
  {
    DelegateType delegateInstance = GetMethod();
    delegateInstance();
    delegateInstance();
  }
}

Из кода видно:

Заметим также, что можно использовать оператор +=, чтобы заставить экземпляр делегата ссылаться на несколько методов сразу (неважно, анонимных или нет):

Example3.cs
using System;

class Program
{
  delegate void DelegateType();

  static void Main()
  {
    DelegateType delegateInstance = 
      delegate() { Console.WriteLine("Hello"); };

    delegateInstance += delegate() { Console.WriteLine("Bonjour"); };

    delegateInstance();
  }
}

Как и следовало ожидать, программа выводит:

Hello
Bonjour

Анонимные методы могут принимать аргументы

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

Example4.cs
class Program
{
  delegate int DelegateType(int valTypeParam, string refTypeParam,
    ref int refParam, out int outParam);

  static DelegateType GetMethod()
  {
    return delegate(int valTypeParam , string refTypeParam,
      ref int refParam , out int outParam   )
    {
      System.Console.WriteLine("Hello valParam:{0} refTypeParam:{1}",
        valTypeParam, refTypeParam);

      refParam++;
      outParam = 9;
      return valTypeParam;
    }; // Конец тела анонимного метода.
  }

  static void Main()
  {
    DelegateType delegateInstance = GetMethod();
    int refVar = 5;
    int outVar;
    int i = delegateInstance(1, "one", ref refVar, out outVar);
    int j = delegateInstance(2, "two", ref refVar, out outVar);
    System.Console.WriteLine("i:{0} j:{1} refVar:{2} outVar:{3}", 
      i, j, refVar, outVar);
  }
}

Программа выводит:

Hello valParam:1 refTypeParam:one
Hello valParam:2 refTypeParam:two
i:1 j:2 refVar:7 outVar:9

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

Анонимный метод не может быть помечен атрибутом. Из-за этого ограничения в списке аргументов анонимного метода нельзя использовать ключевое слово param. Использование ключевого слова param заставляет компилятор помечать метод атрибутом ParamArray.

Example5.cs

using System;

class Program
{
  delegate void DelegateType(params int[] arr);

  static DelegateType GetMethod()
  {
    // Ошибка компиляции: param в данном контексте неприменимо.
    return delegate(params int[] arr){ Console.WriteLine("Hello");};
  }
}

Синтаксические тонкости

В декларации анонимного метода можно обойтись без сигнатуры, т.е. вы не обязаны писать пару скобок после ключевого слова delegate, если метод не принимает аргументов. В этом случае метод должен быть присвоен экземпляру делегата, который возвращает тип void и не имеет out-аргументов. Очевидно, такой анонимный метод не имеет доступа к параметрам, которые присутствуют в вызове экземпляра делегата.

Example6.cs

using System;

class Program 
{
  delegate void DelegateType(int valTypeParam, 
    string refTypeParam, ref int refParam);

  static void Main() 
  {
    DelegateType delegateInstance = delegate 
    { 
      Console.WriteLine("Hello"); 
    };
    int refVar = 5;
    delegateInstance(1, "one", ref refVar);
    delegateInstance(2, "two", ref refVar);
  }
}

Анонимные методы и generic-и

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

Example7.cs
class Foo<T>
{
  delegate void DelegateType(T t);

  internal void Fct(T t) 
  {
    DelegateType delegateInstance = delegate(T arg)
    {
      System.Console.WriteLine("Hello arg:{0}" , arg); 
    };
    delegateInstance(t);
  }
}

class Program 
{
  static void Main() 
  {
    Foo<double> inst = new Foo<double>();
    inst.Fct(5.5);
  }
}

В .NET 2.0 тип делегата может быть объявлен с generic-аргументами. На анонимный метод можно ссылаться с помощью такого делегата.

Example8.cs
class Program
{
  delegate void DelegateType<T>(T t);

  static void Main() 
  {
    DelegateType<double> delegateInstance = delegate(double arg) 
    {
      System.Console.WriteLine("Hello arg:{0}" , arg); 
    };
    delegateInstance(5.5);
  }
}

Анонимные методы в реальном мире

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

Example9.cs
using System.Threading;

class Program
{
  static void Main()
  {
    Thread thread = new Thread(delegate() 
    {
      System.Console.WriteLine("ManagedThreadId:{0} Hello",
      Thread.CurrentThread.ManagedThreadId);
    });
    thread.Start();
    System.Console.WriteLine("ManagedThreadId:{0} Bonjour",
      Thread.CurrentThread.ManagedThreadId);
  }
}

Программа выводит:

ManagedThreadId:1 Bonjour
ManagedThreadId:3 Hello

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

Example10.cs
public class FooForm : System.Windows.Forms.Form 
{
  System.Windows.Forms.Button _button;

  public FooForm() 
  {
    InitializeComponent();

    _button.Click += delegate(object sender, System.EventArgs args) 
    {
      System.Windows.Forms.MessageBox.Show("_button Clicked");
    };
  }
  void InitializeComponent()  {/*...*/}
}

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

Компилятор C# 2.0 и анонимные методы

Простой путь

Как можно предположить, когда компилируется анонимный метод, компилятор создает новый метод в текущем классе:

Example11.cs
class Program 
{
  delegate void DelegateType();

  static void Main() 
  {
    DelegateType delegateInstance = delegate() 
    { 
      System.Console.WriteLine("Hello"); 
    };

    delegateInstance();
  }
}

Ниже показана декомпиляция кода, сгенерированного по предыдущему примеру (с помощью Reflector).


Рисунок 1.

Как видите, был автоматически сгенерирован новый private static-метод с названием <Main>b__0(), содержащий код анонимного метода. (даже если бы этот анонимный метод был декларирован внутри экземплярного метода, он все равно был бы переписан компилятором в статический, так как код анонимного метода не использует никаких членов класса – прим.ред.)

Для ссылки на наш анонимный метод было сгенерировано поле делегата с именем <>9_CachedAnonymousMethodDelegate1 типа DelegateType.

Интересно, что все сгенерированные члены не будут видны в C# intellisense, так как их имена содержат пару угловых скобок <>. Такие имена допустимы в синтаксисе IL/CLR, но некорректны в C#.

Захват локальных переменных

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

Example12.cs
class Program 
{
  delegate int DelegateTypeCounter();

  static DelegateTypeCounter MakeCounter()
  {
    int counter = 0;
    DelegateTypeCounter delegateInstanceCounter = 
      delegate { return ++counter; };
    return delegateInstanceCounter;
  }

  static void Main() 
  {
    DelegateTypeCounter counter1 = MakeCounter();
    DelegateTypeCounter counter2 = MakeCounter();
    System.Console.WriteLine(counter1());
    System.Console.WriteLine(counter1());
    System.Console.WriteLine(counter2());
    System.Console.WriteLine(counter2());
  }
}

Программа выводит:

1
2
1
2

С налету это может поставить в тупик. Кажется, что локальная переменная counter выживает, даже когда поток управления покидает метод MakeCounter(). Даже более того, все выглядит так, будто существуют два экземпляра “выжившей” локальной переменной!

CLR и Intermediate Language в .NET 2.0 напрямую не поддерживают анонимные методы. Всем колдовством занимается компилятор. Это приятный пример синтаксического сахара. Давайте проанализируем сборку (рисунки 2 и 3).


Рисунок 2.


Рисунок 3.

Анализ проясняет ситуацию:

Заметьте, что у метода MakeCounter() нет никаких локальных переменных. Для переменной counter он использует поле с тем же именем в экземпляре сгенерированного класса <>c__DisplayClass1.

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

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

Следующий пример более тонкий, чем можно было ожидать:

Example13.cs
using System.Threading;

class Program 
{
  static void Main() 
  {
    for (int i = 0; i < 5; i++)
      ThreadPool.QueueUserWorkItem(
        delegate { System.Console.WriteLine(i); }, null);
  }
}

Программа осуществляет вывод недетерминированным образом, вроде такого:

0
1
5
5
5

Этот результат заставляет нас сделать вывод, что локальная переменная i делится между всеми потоками. Исполнение недетерминированное, так как метод Main() и наше замыкание (или анонимный метод) исполняются одновременно в нескольких потоках. Чтобы стало ясно, вот декомпилированный код метода Main():

private static void Main()
{
  bool flag1;
  Program.<>c__DisplayClass1 class1 = new Program.<>c__DisplayClass1();
  class1.i = 0;
  goto Label_0030;
Label_000F:
  ThreadPool.QueueUserWorkItem(new WaitCallback(class1.<Main>b__0), null);
  class1.i++;
Label_0030:
  flag1 = class1.i < 5;
  if (flag1) 
  {
    goto Label_000F;
  }
}

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

Example14.cs
using System.Threading;

class Program 
{
  static void Main() 
  {
    for (int i = 0; i < 5; i++)
    {
      int j = i;
      ThreadPool.QueueUserWorkItem(
        delegate { System.Console.WriteLine(j); }, null);
    }
  }
}

На этот раз программа выводит:

0
1
2
3
4

Причина этого поведения в том, что локальная переменная j захватывается на каждой итерации. Вот декомпилированный код метода Main() для этого случая:

private static void Main()
{
  Program.<>c__DisplayClass1 class1;
  bool flag1;
  int num1 = 0;
  goto Label_0029;
Label_0004:
  class1 = new Program.<>c__DisplayClass1();
  class1.j = num1;
  ThreadPool.QueueUserWorkItem(new WaitCallback(class1.<Main>b__0), null);
  num1++;
Label_0029:
  flag1 = num1 < 5;
  if (flag1) 
  {
    goto Label_0004;
  }
}

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

Заметьте, что захваченная локальная переменная больше не является локальной переменной.

Доступ анонимного метода к аргументу внешнего метода

Аргументы метода всегда рассматриваются как локальные переменные. Следовательно, C# 2.0 позволяет анонимному методу использовать аргументы внешнего метода. Вот пример:

Example15.cs
using System;

class Program 
{
  delegate void DelegateTypeCounter();

  static DelegateTypeCounter MakeCounter(string counterName) 
  {
    int counter = 0;
    DelegateTypeCounter delegateInstanceCounter = delegate
    {
      Console.WriteLine(counterName + ++counter);
    };
    return delegateInstanceCounter;
  }

  static void Main() 
  {
    DelegateTypeCounter counterA = MakeCounter("Counter A:");
    DelegateTypeCounter counterB = MakeCounter("Counter B:");
    counterA();
    counterA();
    counterB();
    counterB();
  }
}

Программа выводит:

Counter A:1
Counter A:2
Counter B:1
Counter B:2

Тем не менее, анонимный метод не может захватить out- и ref-аргументы. (out- и ref-параметры физически реализуются с помощью указателей. Так как время жизни экземпляра класса, сгенерированного для анонимного метода, может в общем случае не совпадать со временем жизни переменных, на которые должны ссылаться out- и ref-параметры, то попытка сослаться на них небезопасна, а стало быть, недопустима. – прим.ред.)

Доступ из анонимного метода к члену внешнего класса

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

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

Example16.cs
delegate void DelegateTypeCounter();

class CounterBuilder
{
  string _name; // Поле экземпляра

  internal CounterBuilder(string name) { _name = name; }

  internal DelegateTypeCounter BuildCounter(string counterName) 
  {
    int counter = 0;
    DelegateTypeCounter delegateInstanceCounter = delegate 
    {
      System.Console.Write(counterName + ++counter);
      // Можно было бы написать this. _name.
      System.Console.WriteLine(" Counter built by: " + _name); 
    };
    return delegateInstanceCounter;
  }
}

class Program 
{
  static void Main() 
  {
    CounterBuilder cBuilder1 = new CounterBuilder("Factory1");
    CounterBuilder cBuilder2 = new CounterBuilder("Factory2");
    DelegateTypeCounter cA = cBuilder1.BuildCounter("Counter A:");
    DelegateTypeCounter cB = cBuilder1.BuildCounter("Counter B:");
    DelegateTypeCounter cC = cBuilder2.BuildCounter("Counter C:");
    cA();  cA ();
    cB();  cB();
    cC();  cC();
  }
}

Программа выводит:

Counter A:1 Counter built by: Factory1
Counter A:2 Counter built by: Factory1
Counter B:1 Counter built by: Factory1
Counter B:2 Counter built by: Factory1
Counter C:1 Counter built by: Factory2
Counter C:2 Counter built by: Factory2

Декомпилируем метод MakeCounter(), чтобы убедиться в захвате ссылки this:

internal DelegateTypeCounter BuildCounter(string counterName)
{
    CounterBuilder.<>c__DisplayClass1 class1 = new 
                                 CounterBuilder.<>c__DisplayClass1();
    class1.<>4__this = this;
    class1.counterName = counterName;
    class1.counter = 0;
    return new DelegateTypeCounter(class1.<BuildCounter>b__0);
}

Обратите внимание, что ссылка this не может быть захвачена анонимным методом внутри структуры. Компилятор выдает ошибку: “Anonymous methods inside structs cannot access instance members of ‘this’. Consider copying ‘this’ to a local variable outside the anonymous method and using the local instead.”

Анонимные методы и замыкания

Определения: замыкание и лексическое окружение

Замыкание (closure) – это функция, которая захватывает значения своего лексического окружения, когда она создается во время исполнения. Лексическое окружение функции есть множество локальных переменных, полей и других членов классов, видимых из этой функции.

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

Определение замыкания подразумевает понятие создания функции во время исполнения. Основные императивные языки, такие как C, C++, C# 1.Х, Java или VB.NET 1.0 не поддерживают создания экземпляров функции во время исполнения. Эта возможность заимствована из функциональных языков, таких как Haskell или Lisp. Таким образом, поддерживая замыкания, C# 2.0 выходит за пределы императивных языков. Тем не менее, C# 2.0 не является первым императивным языком, который поддерживает замыкания, поскольку Perl и Ruby также обладают такой возможностью.

Прогулка по замыканиям

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

Обычно, когда используются объектно-ориентированные языки, контекстом функции (в данном случае контекстом метода исполнения) является состояние объекта, в котором она вызывается. Когда программируют на не объектно-ориентированном языке (например, на C), контекст функции – это значения глобальных переменных. Когда имеют дело с замыканиями, контекст – это значения захваченных переменных на момент создания замыкания. Следовательно, по аналогии с классами, замыкания есть способ ассоциации поведения и данных. В объектно-ориентированном мире методы и данные ассоциированы, благодаря ссылке this. В функциональном мире функция ассоциируется со значениями захваченных переменных. Чтобы стало понятнее:

Использование замыканий вместо классов

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

Example17.cs
class Program 
{
  delegate void DelegateMultiplier(ref int integerToMultipl);

  static DelegateMultiplier BuildMultiplier (int multiplierParam) 
  {
    return delegate(ref int integerToMultiply) 
    {
      integerToMultiply *= multiplierParam;
    };
  }

  static void Main() 
  {
    DelegateMultiplier multiplierBy8 = BuildMultiplier(8);
    DelegateMultiplier multiplierBy2 = BuildMultiplier(2);
    int anInteger = 3;

    multiplierBy8(ref anInteger);
    // Здесь anInteger равно 24.
    multiplierBy2(ref anInteger);
    // Здесь anInteger равно 48.
  }
}

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

Example18.cs
using System;

class Article 
{
  public Article(decimal price) { _price = price; }

  private decimal _price;
  public decimal Price { get { return _price; } }
}

class Program 
{
  delegate decimal DelegateTaxComputer(Article article);

  static DelegateTaxComputer BuildTaxComputer(decimal tax) 
  {
    return delegate(Article article) 
    {
      return (article.Price * (100 + tax)) / 100;
    };
  }

  static void Main()
  {
    DelegateTaxComputer taxComputer19_6 = BuildTaxComputer(19.6m);
    DelegateTaxComputer taxComputer5_5 = BuildTaxComputer(5.5m);
    Article article = new Article(97);

    Console.WriteLine("Price TAX 19.6% : " + taxComputer19_6(article));
    Console.WriteLine("Price TAX  5.5% : " + taxComputer5_5(article));
  }
}

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

Делегаты и замыкания

Ранее мы отметили, что понятие делегата, используемого совместно с экземпляром метода в .NET 1.x, концептуально близко к понятию замыкания. Фактически, такой делегат ссылается сразу на данные (состояние объекта) и на поведение. Но есть ограничение: поведением должен быть метод экземпляров класса, определяющего тип ссылки this.

Это ограничение в .NET 2 сведено к минимуму. Благодаря нескольким перегрузкам метода Delegate.CreateDelegate() вы можете отныне подменить ссылку на this ссылкой на первый аргумент статического метода. Например:

Example19.cs
class Program 
{
  delegate void DelegateType(int writeNTime);

  // Этот метод – public, чтобы избежать проблем с 
  // доступом к не-public членам через рефлексию
  public static void WriteLineNTimes(string s, int nTime) 
  {
    for(int i=0; i < nTime; i++)
      System.Console.WriteLine(s);
  }

  static void Main() 
  {
    DelegateType deleg = System.Delegate.CreateDelegate(
      typeof(DelegateType),
      "Hello",
      (DelegateType)typeof(Program).GetMethod("WriteLineNTimes"));

    deleg(4);
  }
}

Программа выводит:

Hello
Hello
Hello
Hello

Внутренняя реализация делегатов была полностью пересмотрена в .NET Framework 2.0 и CLR. Хорошей новостью является то, что вызов метода через делегат стал значительно более быстрым.

Анонимные методы и функторы

Введение в функторы

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

namespace System 
{
  public delegate void Action<T> (T obj); 
  public delegate bool Predicate<T> (T obj); 
  public delegate U    Converter<T,U> (T from); 
  public delegate int  Comparison<T> (T x, T y);
}

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

Example20.cs
using System.Collections.Generic;

class Program 
{
  class Article 
  {
    public Article(decimal price, string name) { Price = price; Name = name; }
    public readonly decimal Price;
    public readonly string  Name;
  }

  static bool IsEven(int i) { return i % 2 == 0; }
  static int sum = 0;
  static void AddToSum(int i) { sum += i; }

  static int CompareArticle(Article x, Article y)
  {
    return Comparer<decimal>.Default.Compare(x.Price, y.Price);
  }

  static decimal ConvertArticle(Article article)
  {
    return (decimal)article.Price;
  }

  static void Main()
  {
    List<int> integers = new List<int>();

    for(int i=1; i<=10; i++) 
      integers.Add(i);

    // Поиск нечетных чисел.
    // Неявно использует объект делегата ‘Predicate<T>’ 
    List<int> even = integers.FindAll(IsEven);

    // Суммирование элементов списка.
    // Неявно использует объект делегата ‘Action<T>’.
    integers.ForEach(AddToSum);

    List<Article> articles = new List<Article>();
    articles.Add(new Article(5,"Shoes"));
    articles.Add(new Article(3,"Shirt"));

    // Сортировка элементов типа ‘Article’.
    // Неявно использует объект делегата ‘Comparison<T>’.
    articles.Sort(CompareArticle);

    // Приведение элементов типа ‘Article’ к ‘decimal’.
    // Неявно использует объект делегата ‘Converter<T,U>’.
    List<decimal> artPrice = articles.ConvertAll<decimal>(ConvertArticle);
  }
}

Читатели, которые уже использовали Standard Template Library (STL) в C++, узнают понятие функтора. Функторы особенно полезны для выполнения одинаковых операций над всеми элементами коллекции. В C++ для реализации функтора перегружали оператор скобок. В .NET функтор принимает форму экземпляра делегата. В предыдущей программе четыре неявно созданных экземпляра делегата были примерами четырех функторов.

ПРИМЕЧАНИЕ

По нашему мнению, использование термин «функтор» излишне в мире .NET. Это вносит путаницу, проще использовать термин «делегат» во избежание неоднозначностей. – прим. ред.

Использование анонимных методов и функторов для запросов к коллекциям

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

Example21.cs
class Program 
{
  class Article 
  {
    public Article(decimal price,string name){Price = price;Name = name;}
    public readonly decimal Price;
    public readonly string  Name;
  }

  static void Main()
  {
    // Поиск нечетных чисел.
    // Неявно использует объект делегата ‘Predicate<T>’
    List<int> integers = new List<int>();

    for(int i=1; i<=10; i++) 
      integers.Add(i);

    List<int> even =integers.FindAll(delegate(int i){ return i%2==0; });

    // Суммирование элементов списка.
    // I Неявно использует объект делегата ‘Action<T>’.
    int sum = 0;
    integers.ForEach(delegate(int i) { sum += i; });

    // Сортировка элементов типа ‘Article’.
    // Неявно использует объект делегата ‘Comparison<T>’.
    List<Article> articles = new List<Article>();
    articles.Add(new Article(5,"Shoes"));
    articles.Add(new Article(3,"Shirt"));
    articles.Sort(delegate(Article x, Article y)
      {
        return Comparer<decimal>.Default.Compare(x.Price,y.Price);
      });

    // Приведенеи элементов типа ‘Article’ к ‘decimal’.
    // Неявно использует объект делегата ‘Converter<T,U>’.
    List<decimal> artPrice = articles.ConvertAll<decimal> (
      delegate(Article article) { return (decimal)article.Price; });
  }
}

Поддержка функторов в классах List и Array

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

public class List<T> : ... 
{
   public int FindIndex(Predicate<T> match);
   public int FindIndex(int index, Predicate<T> match);
   public int FindIndex(int index, int count, Predicate<T> match);

   public int FindLastIndex(Predicate<T> match);
   public int FindLastIndex(int index, Predicate<T> match);
   public int FindLastIndex(int index, int count, Predicate<T> match);

   public List<T> FindAll(Predicate<T> match); 
   public T Find(Predicate<T> match);
   public T FindLast(Predicate match);

   public bool Exists(Predicate<T> match);
   public bool TrueForAll(Predicate<T> match); 
 
   public int RemoveAll(Predicate<T> match);
   public void ForEach(Action<T> action); 
   public void Sort(Comparison<T> comparison);
   public List<U> ConvertAll<U>(Converter<T,U> converter);
   ...
}

public class Array 
{
   public static int FindIndex<T>(T[] array, int startIndex, 
                                  int count, Predicate<T> match);
   public static int FindIndex<T>(T[] array, int startIndex, 
                                  Predicate<T> match);
   public static int FindIndex<T>(T[] array, Predicate<T> match);

   public static int FindLastIndex<T>(T[] array, int startIndex, 
                                      int count, Predicate<T> match);
   public static int FindLastIndex<T>(T[] array, int startIndex, 
                                      Predicate<T> match);
   public static int FindLastIndex<T>(T[] array, Predicate<T> match);

   public static T[] FindAll<T>(T[] array, Predicate<T> match);
   public static T Find<T>(T[] array, Predicate<T> match);
   public static T FindLast<T>(T[] array, Predicate<T> match);

   public static bool Exists<T>(T[] array, Predicate<T> match);
   public static bool TrueForAll<T>(T[] array, Predicate<T> match);

   public static void ForEach<T>(T[] array, Action<T> action);
   public static void Sort<T>(T[] array, System.Comparison<T> comparison);
   public static U[] ConvertAll<T, U>(T[] array, 
                                       Converter<T, U> converter);
   ...
}


Эта статья опубликована в журнале RSDN Magazine #2-2006. Информацию о журнале можно найти здесь