Делегаты

Описание

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

С архитектурной точки зрения делегаты решают задачу абстракции реализации функции с определённой сигнатурой посредством определения контракта взаимодействия с внешним кодом. Использование делегатов вместо явного вызова функций позволяет устранить жесткую привязку (на этапе компиляции) использующего их кода к конкретной реализации функции. Кроме того, появляется возможность определять вызываемую функцию динамически (во время исполнения программы). Отсутствие привязки на этапе компиляции означает возможность использование кода из внешних библиотек, т.е. реализация API (интерфейс программирования) или внешних инструментов (plug-ins). Данный подход также известен как шаблон проектирования функтор.

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

Пример использования делегатов в классе FCL List, абстракция механизма поиска элемента в массиве от критерия или способа поиска.

public delegate bool Predicate<T>(T obj);

public bool Exists(Predicate<T> match);

Технические детали реализации

При определении делегата компилятором автоматически создается запечатанный (sealed) класс наследник System.Delegate (в С# все делегаты по умолчанию являются наследниками класса System.MulticastDelegate). Классы System.Delegate и System.MulticastDelegate абстрактные, соответственно создать объекты данных классов не получится. Явное наследование от данных классов запрещено, нарушение данного правила (объявление класса, являющегося наследником System.Delegate) приводит к ошибке компиляции CS0644:

"класс1" не может наследовать от специального класса "класс2" ('class1' cannot derive from special class 'class2'). Классы не могут явно наследовать от любых базовых классов, перечисленных ниже.
System.Enum
System.ValueType
System.Delegate
System.Array

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

public delegate double TransformDelegate(double v);

делегат

Создание делегатов

Конструктор делегата принимает два параметра: ссылку на объект, в том случае, если функция является методом или null, если вызываемая функция статическая, и IntPtr содержащий указатель на функцию. Адрес метода определяется вызовом ldfn или ldvirtfn.

ldftn

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

ldvirtftn

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

Пример инициализации делегатов

Рассмотрим простой пример инициализации делегата TransformDelegate. Ниже приведен пример C# кода и разобран генерируемый IL код в случае инициализации статическим методом (Null), методом класса (Ground) и виртуальным методом (Mult).

namespace netnomicon.delegates
{
  public delegate double TransformDelegate(double v);

  public abstract class Evaluator
  {
    protected TransformDelegate _callback;

    public Evaluator()
    {
      //_callback = Null;
      //_callback = Ground;
      //_callback = Mult;
    }

    public static double Null(double value)
    {
      return double.NaN;
    }

    public double Ground(double value)
    {
      return value * 0;
    }

    public abstract double Mult(double value);
  }
}

Инициализация статической функцией класса

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

public Evaluator()
{
//IL_0000: ldarg.0 помещает this на вершину стэка;
//вызов конструктора System.Object который извлечет из стэка аргумент 0 (this);
//IL_0001: call instance void [mscorlib]System.Object::.ctor()

_callback = Null;

//заново помещает this на вершину стэка, поскольку он был извлечен
//конструктором Object на шаге IL_0001;
//IL_0008: ldarg.0 
//помещает null на вершину стэка, поскольку Evaluator.Null статический 
//метод и для его вызова не нужен экземпляр объекта;
//IL_0009: ldnull 
//помещает на вершину стэка указатель на машинный код;
//IL_000a: ldftn float64 netnomicon.delegates.Evaluator::Null(float64)

//выделяет память и вызывает конструктор делегата;
//Конструктор извлечет из стэка адрес объекта, выделенный newobj 
//и аргументы переданные в стэк инструкциями IL_0009, IL_000a; 
//адрес нового объекта помещается на вершину стэка;
//IL_0010: newobj instance void 
           netnomicon.delegates.TransformDelegate::.ctor(object, native int)

//К данному врпемени стэк имеет следующее состояние 
//ссылка на объект :значение записанное на шаге IL_0008
//значение         :значение записанное на шаге IL_0010
//инициализация поля _callback объекта Evaluator экземпляром делегата
//IL_0015: stfld class netnomicon.delegates.TransformDelegate 
           netnomicon.delegates.Evaluator::_callback

Инициализация делегата методом

Инициализация делегата методом класса идентична инициализации его статической функцией, за исключением шагов IL_0009 и IL_000a

_callback = Ground;

// IL_0009: ldarg.0 : в конструктор делегата передается не null а this
// IL_000a: ldftn instance float64 netnomicon.delegates.Evaluator::Ground(float64)

Инициализация делегата вирутальным методом

Инициализация делегата виртуальным методом класса идентична инициализации его методом класса, за исключением способа получения адреса функции. Поскольку для вычисления адреса виртуального метода нужен указатель на объект, его копия, записанная в стэк на шаге IL009? помещается в стэк вызовом оператора dup, после чего вызывается оператор ldvirtfn.

_callback = Mult;
// В данном случае изменился способ получения адреса функции
// Адрес определяется динамически, вызовом ldvirtftn
// IL_000a: dup
// IL_000b: ldvirtftn instance float64 netnomicon.delegates.Evaluator::Mult(float64)

Инициализация делегатов присвоением

Метод или делегат T является присваиваемым делегату D если:

Ковариация

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

Контрвариация

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

Пример

namespace netnomicon.delegates
{
  public class A {}
  public class B : A { }
  public class C { }

  public delegate A CreatorA();
  public delegate B CreatorB();
  public delegate C CreatorC();

  public delegate void EventHandlerA(A v);
  public delegate void EventHandlerB(B v);
  public delegate void EventHandlerC(C v);

class DelegatesAssignment
{
  public static A CreateA()
  {
    return null;
  }

  public static B CreateB()
  {
    return null;
  }
  public static C CreateC()
  {
    return null;
  }

  public static void ProcessA(A value){}
  public static void ProcessB(B value){}
  public static void ProcessC(C value){}

  public static void Test()
  {
    #region ковариация

    CreatorA ca1 = CreateA;
    CreatorA ca2 = CreateB;
    //CS0407: CreatorA ca3 = CreateC;

    //CS0407: CreatorB cb1 = CreateA;
    CreatorB cb2 = CreateB;
    //CS0407: CreatorB cb3 = CreateC;

    //CS0407: CreatorC cc1 = CreateA;
    //CS0407: CreatorC cc2 = CreateB;
    CreatorC cc3 = CreateC;

    #endregion

    #region контрвариация

    EventHandlerA pa1 = ProcessA;
    //CS0123: ProcessorA pa2 = ProcessB;
    //CS0123: ProcessorA pa3 = ProcessC;

    EventHandlerB pb1 = ProcessA;
    EventHandlerB pb2 = ProcessB;
    //CS0123: ProcessorB pb3 = ProcessC;

    //ProcessorC pc1 = ProcessA;
    //ProcessorC pc2 = ProcessB;
    EventHandlerC pc3 = ProcessC;

    #endregion
  }
}
}