Привет, Дорогой читатель!


Почти все кто мало-мальски работал в .Net знает что такое Делегаты (Delegates). А те кто не знает о них, почти наверняка хотя бы в курсе о Лямбда-выражениях (Lambda expressions). Но лично я постоянно то забываю о синтаксисе их объявления, то возвращаюсь к многостраничным объяснениям умных людей о том, как компилятор реагирует на подобные конструкции. Если у Вас случается такая проблема, то милости прошу!

Делегаты


Делегат это особый тип. И объявляется он по особому:

delegate int MyDelegate (string x);

Тут все просто, есть ключевое слово delegate, а дальше сам делегат с именем MyDelegate, возвращаемым типом int и одним аргументом типа string.

По факту же при компиляции кода в CIL — компилятор превращает каждый такой тип-делегат в одноименный тип-класс и все экземпляры данного типа-делегата по факту являются экземплярами соответствующих типов-классов. Каждый такой класс наследует тип MulticastDelegate от которого ему достаются методы Combine и Remove, содержит конструктор с двумя аргументами target (Object) и methodPtr (IntPtr), поле invocationList (Object), и три собственных метода Invoke, BeginInvoke, EndEnvoke.

Объявляя новый тип-делегат мы сразу через синтаксис его объявления жестко определяем сигнатуру допустимых методов, которыми могут быть инициализированы экземпляры такого делегата. Это сразу влияет на сигнатуру автогенерируемых методов Invoke, BeginInvoke, EndEnvoke, поэтому эти методы и не наследуются от базового типа а определяются для каждого типа-делегата отдельно.

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

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

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

void MyFunc(myDelegate deleg, int arg){deleg.Invoke(arg);}

Создавая в коде экземпляр делегата его конструктору передается метод (подойдет и экземплярный и статичческий, главное чтобы сигнатура метода совпадала с сигнатурой делегата). Если метод экземплярный то в поле target записывается ссылка на экземпляр-владелец метода (он нужен нам, ведь если метод экземплярный то это как минимум подразумевает работу с полями этого объекта target), а в methodPtr ссылка на метод. Если метод статический то записываются в поля target и methodPtr будут записаны null и ссылка на метод соответственно.

Инициализировать переменную делегата можно через создание экземпляра делегата:

MyDeleg x = new MyDeleg(MyFunc);

Или упрощенный синтаксис без вызова конструктора:

MyDeleg x = MyFunc;

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

Методы делегатов:

Invoke — синхронное выполнение метода который храниться в делегате.
BeginInvoke, EndEnvoke — аналогично но асинхронное.

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

delegInst.Invoke(argument);

это аналогично записи:

delegInst(argument);

А зачем делегату поле invocationList?


Поле invocationList имеет значение null для экземпляра делегата пока делегат хранит ссылку на один метод. Этот метод можно всегда перезаписать на другой приравняв через "=" переменной новый экземпляр делегата (или сразу нужного нам метода через упрощенный синтаксис). Но так же можно создать цепочку вызовов, когда делегат хранит ссылки на более чем один метод. Для этого нужно вызвать метод Combine:

MyDeleg first = MyFunc1;
MyDeleg second = MyFunc2;
first = (MyDeleg) Delegate.Combine(first, second);

Метод Combine возвращает ссылку на новый делегат в котором поля target и methodPtr пусты, но invocationList, который содержит две ссылки на делегаты: тот что был раньше в переменной first и тот что еще хранится в second. Надо понимать что добавив третий делегат через метод Combine и записав его результат в first, то метод вернет ссылку на новый делегат с полем invocationList в котором будет коллекция из трех ссылок, а делегат с двумя ссылками будет удален сборщиком мусора при следующем цикле очистки.

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

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

Deleg first = first.Remove(MyFunc2);

Переопределенные для делегатов операторы += и -= являются аналогами методов Combine и Remove:

first = (Deleg) Delegate.Combine(first, second);

аналогично записи:

first += MyFunc2;
first = first.Remove(MyFunc2);

аналогично записи:

first -= MyFunc;

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

Также стоит упомянуть что библиотека FCL уже содержит наиболее популярные типы делегатов (обобщенные и нет). Например делегат Action<T> представляет собой метод без возвращаемого значения но с аргументом, а Fucn<T, TResult> и с возвращаемым значением и аргументом.

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


Так же экземпляр делегата можно инициализировать лямбда-выражением (lambda-expression). Стоит что они были введены в C# 3.0, а до них существовали анонимные-функции появившиеся в C# 2.0.

Отличительной чертой лямбд является оператор =>, который делит выражение на левую часть с параметрами и правую с телом метода.

Допустим у нас есть делегат:

delegate string MyDeleg (string verb);

Синтаксис лямбд такой:

MyDeleg myDeleg = (string x) => {x + "world!"};

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

MyDeleg myDeleg = (x) => {x + x};

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

MyDeleg myDeleg = x => x + x;

Если в сигнатуре делегата аргументов нет то необходимо указать пустые скобки:

MyDeleg myDeleg = () => x + x;

А что о лямбдах думает компилятор?


Важно понимать что лямбда выражения не являются волшебными строками передающимися напрямую в делегат. На самом деле на этапе компиляции каждое такое выражение превращается в анонимный private метод с именем начинающимся на "<" что исключает возможность вызова такого метода напрямую. Этот метод всегда является членом типа в котором вы используете данное лямбда выражение, и передается в конструктор делегата явно в CIL коде.

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

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

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

В итоге


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

Отдельное спасибо великому Джеффри Рихтеру, который конечно статью не прочтет, но книгу «CLR via C#» которого я использую с большим удовольствием, в том числе и при написании данной шпаргалки.

Всем большое спасибо!
Поделиться с друзьями
-->

Комментарии (5)


  1. mayorovp
    31.05.2017 12:16
    +4

    Если метод экземплярный то в поле target записывается ссылка на экземпляр-владелец метода (он нужен нам, ведь если метод экземплярный то это как минимум подразумевает работу с полями этого объекта target), а в methodPtr ссылка на метод. Если метод статический то записываются в поля target и methodPtr будут записаны null и ссылка на метод соответственно.

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


    Этой возможностью компилятор C# пользуется при создании делегатов на методы-расширения. Также ее удобно использовать совместно с компиляцией деревьев выражений через CompileToMethod, потому что методы экземпляров там толком не поддерживаются.




    Также не могу не напомнить, что лямбда-выражение не обязано быть делегатом, оно может быть и типа Expression<>


    1. Deosis
      01.06.2017 07:36
      +1

      В обратную сторону тоже работает и экземплярные методы можно использовать как статические (без сохранения target). А при вызове передавать объект первым параметром.


  1. littleant
    31.05.2017 22:00
    +1

    Перечитайте свой текст заново. Много ошибок, большинство по невнимательности.

    А за статью спасибо, содержательная статья.


  1. kefirr
    01.06.2017 10:13

    Получается довольно бессмысленно, но работает.
    void MyFunc(myDelegate deleg, int arg){deleg.Invoke(arg);}

    Очень даже не бессмысленно.


    Пусть нам нужно выполнить тяжёлую обработку большого набора данных. Такой метод может распараллеливать вычисления автоматически между потоками (PLINQ, Parallel.ForEach) или компьютерами (Ignite):
    IEnumerable<TRes> Apply<TArg, TRes>(Func<TArg, TRes> func, IEnumerable<TArg> args)


    P.S. .Net -> .NET


  1. Sinatr
    01.06.2017 12:06
    +2

    Ну блин, так хорошо начали…

    MyDeleg myDeleg = (string x) => { x + "world!"};
    
    CS1643 Not all code paths return a value in lambda expression

    Правило простое, если лямбду поместить в { } (блок кода?), например, то она автоматически становится анонимной функцией и снова требует ключевое слово return:
    MyDeleg @delegate = x => { return x + "world!"; };