Делегаты это обертка над функцией, которая принимает указатель на саму функции и контекст, если в коде создать делегат вида:
public delegate int TestDelegate(int a, int b);
Его объявление в IL будет такое:
class public sealed auto ansi
Resolvers.Tests.Delegates.TestDelegate
extends [System.Runtime]System.MulticastDelegate
{
.method public hidebysig specialname rtspecialname instance void
.ctor(
object 'object',
native int 'method'
) runtime managed
{
// Can't find a body
} // end of method TestDelegate::.ctor
.method public hidebysig virtual newslot instance int32
Invoke(
int32 a,
int32 b
) runtime managed
{
// Can't find a body
} // end of method TestDelegate::Invoke
.method public hidebysig virtual newslot instance class [System.Runtime]System.IAsyncResult
BeginInvoke(
int32 a,
int32 b,
class [System.Runtime]System.AsyncCallback callback,
object 'object'
) runtime managed
{
// Can't find a body
} // end of method TestDelegate::BeginInvoke
.method public hidebysig virtual newslot instance int32
EndInvoke(
class [System.Runtime]System.IAsyncResult result
) runtime managed
{
// Can't find a body
} // end of method TestDelegate::EndInvoke
} // end of class Resolvers.Tests.Delegates.TestDelegate
Встречая такой делегат, IL генерирует класс по имени делегата, в котором реализует методы:
Invoke
BeginInvoke
EndInvoke
В .net core вызов последних двух методов невозможен, последние два метода - устаревшая парадигма асинхронного программирования в .net, на ее смену пришли Task'и, поэтому остановимся на методе Invoke
Invoke - вызывает переданный в делегат метод(либо цепочку делегатов)
Конструктор делегата принимает object target и int method - указатель на метод, также интересно, что в IL также нет реализации делегатов (я про can't find body), как вызываются делегаты вопрос(https://stackoverflow.com/questions/59485280/why-cant-i-find-the-invoke-method-body-of-a-delegate-in-il-code)
MulticastDelegate - наследник Delegate, он расширяет базовый класс добавляя object? _invocationList, который заполняется при вызове Delegate.Combine(который кстати находится в классе Delegate и вызывает шаблонный метод CombineImpl который бросает исключение в Delegate и переопределен в MulticaseDelegate), но CLR по умолчанию создает класс который наследуется от MulticastDelegate.
Основные два поля в делегатах - _methodPtr, и target. Если метод статический то target == null
Интересно что класс MulticastDelegate, который является предком сгенерированного в IL класса TestDelegate, в своем конструкторе принимает уже имя метода, как вызывается Delegate не совсем понятно(как работает метод Invoke)
Но делегаты можно объединять в цепочки, тогда при вызове Invoke будет вызвана вся цепочка:
TestDelegate chain = (TestDelegate) Delegate.Combine(SumDelegate, MultiplyDelegate);
var r = chain.Invoke(1, 2);
При этом результат - вызов последнего метода в цепочке, сама цепочка хранится в предке сгенерированного класса MultiCaseDelegate (_invocationList), при каждом побавлении-удалении из из цепочки делегатов - возвращается новый делегат, если в цепочке остается один _invocationList очищается - остается только метод в самом делегате (Method). Если _invocationList содержит элементы метод в самом делегате игнорируется(потому что он уже добавлен в invocationList, также функция Remove удаляет только один экземпляр делегата (на случай если добавлены одинаковые - будет удален только один)
Изначально в .net'e были именованные делегаты, но вскоре стало понятно, что все они содержат один тип и каждый раз создавать делегат с одной сигнотурой и другим именем - лишняя работа. Так были введены Action'ы и Func'и, все и Action и Func делегатов - по 16 штук. Они различаются generic параметрами:
Action<in T>
Action<in T,in T1>
...
Action<in T, in T1, ... in T15>
///
Func<out TResult>
Func<in T,out TResult>
...
Func<in T, in T1, ... out T15>
Также делегаты всегда были неудобны излишней громоздкостью:
private void InvokeTestForDelete()
{
TestForDelegate(new TestDelegate(GetSum))
}
public int GetSum(int a, int b)
{
return a + b;
}
public int TestForDelegate(TestDelegate testDelegate)
{
return testDelegate.Invoke(1, 2);
}
Для того, чтобы вызвать метод TestForDelegate пришлось создать метод GetSum и передать в конструктор делегата TestDelegate. Это добавляет в класс ненужные методы(которые нужны только для того, чтобы передать их в другой метод, который может даже не находится в этом классе. Первое упрощение в том, что мы можем сделать вот так:
private void InvokeTestForDelete()
{
TestForDelegate(GetSum);
}
IL Code:
private void InvokeTestForDelete()
{
// ISSUE: method pointer
this.TestForDelegate(new TestDelegate((object) this, __methodptr(GetSum)));
}
Как можно видеть в итоге все-равно создается делегат Второе упрощение:
private void InvokeTestForDelete()
{
TestForDelegate((a, b) => a + b);
}
IL Code:
private void InvokeTestForDelete()
{
// ISSUE: method pointer
this.TestForDelegate(DelegateTest.\u003C\u003Ec.\u003C\u003E9__6_0 ?? (DelegateTest.\u003C\u003Ec.\u003C\u003E9__6_0 = new TestDelegate((object) DelegateTest.\u003C\u003Ec.\u003C\u003E9, __methodptr(\u003CInvokeTestForDelete\u003Eb__6_0))));
}
[CompilerGenerated]
[Serializable]
private sealed class \u003C\u003Ec
{
public static readonly DelegateTest.\u003C\u003Ec \u003C\u003E9;
public static TestDelegate \u003C\u003E9__6_0;
static \u003C\u003Ec()
{
DelegateTest.\u003C\u003Ec.\u003C\u003E9 = new DelegateTest.\u003C\u003Ec();
}
public \u003C\u003Ec()
{
base.\u002Ector();
}
internal int \u003CInvokeTestForDelete\u003Eb__6_0(int a, int b)
{
return a + b;
}
}
А здесь ситуация интереснее, вообще то, как реализованы лямбы - интересно, особенно как реализованы замыкания в лямбдах, в данном случае сгенерирован дополнительный класс в котором добавлены два поля. Читать код в такой кодировке сложно, но основное что происходит:
- Создается новый класс, в нем из лямбды создается метод
- Создается поле, для того, чтобы поместить в него делегат, который будет создан в методе InvokeTestForDelegate(закешировать, если вызовов метода InvokeTestForDelegate будет много
- Создается поле которое можно будет поместить в target при создании делегата в методе InvokeTestForDelegate(первый аргумент конструктора) На самом деле решение создать класс странно - вполне можно было разместить автосгенерированный метод в данном классе
Интересное начинается когда в лямбду попадают локальные переменные метода
Новый метод:
private void InvokeTestForDelete()
{
var c = 1;
TestForDelegate((a, b) => a + b + c);
Console.WriteLine(c);
}
// Измененный код этого метода:
private void InvokeTestForDelete()
{
DelegateTest.\u003C\u003Ec__DisplayClass6_0 cDisplayClass60 = new DelegateTest.\u003C\u003Ec__DisplayClass6_0();
cDisplayClass60.c = 1; // инициализация поля в автосгенерированном классе
// ISSUE: method pointer
this.TestForDelegate(new TestDelegate((object) cDisplayClass60, __methodptr(\u003CInvokeTestForDelete\u003Eb__0)));
Console.WriteLine(cDisplayClass60.c); // обращение идет к полю нового класса, потому что функция в лямбде может поменять его значение
}
// Сгенерированный код лямбды
[CompilerGenerated]
private sealed class \u003C\u003Ec__DisplayClass6_0
{
public int c;
public \u003C\u003Ec__DisplayClass6_0()
{
base.\u002Ector();
}
internal int \u003CInvokeTestForDelete\u003Eb__0(int a, int b)
{
return a + b + this.c;
}
}
Интересно, что в таком случае, не создается никаких полей для кеширования и для target значения в сгенерированном классе, почему? Потому что возможен доступ к этим экземплярам из разных потоков, например один поток будет передавать в c значение 2, а другой 3, получается, что будет гонка. По этому target не создается и значение не кешируется А вот что будет если в замыкание попадет поле класса:
private void InvokeTestForDelete()
{
TestForDelegate((a, b) => a + b + _value);
Console.WriteLine(_value);
}
[CompilerGenerated]
private int \u003CInvokeTestForDelete\u003Eb__7_0(int a, int b)
{
return a + b + this._value;
}
private void InvokeTestForDelete()
{
// ISSUE: method pointer
this.TestForDelegate(new TestDelegate((object) this, __methodptr(\u003CInvokeTestForDelete\u003Eb__7_0)));
Console.WriteLine(this._value);
}
Компилятор также создаст делегат в методе TestForDelegate, как и в случае, когда мы подавали просто Sum, а не лямбду и будет создан метод в этом же классе, это разумно, потому что сдесь не нужен автосгенерированный класс - до полей класса можно дотянуться и из текущего. Также можно заметить, что делегат не кешируется, по-видимому кеширование в данном случае организуется не так легко - потому что нужно сделать в классе дополнителное поле, которое заполнить только при первом обращении. Это только моя версия, я не знаю насколько она верна(https://stackoverflow.com/questions/59496798/why-when-calling-labmda-that-uses-class-field-delegate-not-cached)