Skip to content

Instantly share code, notes, and snippets.

@eterekhin
Last active December 27, 2019 06:20
Show Gist options
  • Save eterekhin/37909a5541261b5b71f8ffca9fbe7021 to your computer and use it in GitHub Desktop.
Save eterekhin/37909a5541261b5b71f8ffca9fbe7021 to your computer and use it in GitHub Desktop.

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

    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;
      }
    }

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

  1. Создается новый класс, в нем из лямбды создается метод
  2. Создается поле, для того, чтобы поместить в него делегат, который будет создан в методе InvokeTestForDelegate(закешировать, если вызовов метода InvokeTestForDelegate будет много
  3. Создается поле которое можно будет поместить в 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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment