Делегаты в C# подробнее

от автора

Всем привет, эта статья создана для ребят, которые знакомы с делегатами, но хотели бы разобраться, что это поподробнее.

Что же такое делегаты?

Делегаты — это указатели на методы

Такое понятие нам дает практически каждый сайт, на который мы перейдем по запросу «Делегаты C#».

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

Сами методы хранятся в метаданных класса/структуры. «ссылки на методы» для делегатов хранятся в хипе. Но только лишь ссылка на метод храниться в хипе у делегата или что — то еще? Давайте проверим

Создадим простой класс счетчика

class Counter {     public int Num = 0;      public void Iteration()     {         Num++;         Console.WriteLine(Num + " Из Iteration");     } }

Здесь у нас при вызове функции переменная Num увеличивается на единичку и потом значение выводится на экран

Теперь возьмем делегат и назначим его к нашему методу Iteration. Вызовем метод Invoke у делегата и выведем на экран значение Num у экземпляра нашего класса

public static void Main()     {         Counter cnt = new Counter();         Action act = cnt.Iteration;         act();          Console.WriteLine(cnt.Num + " Из Main");     }

На экране увидим:
1 Из Iteration
1 Из Main

Результат логичный, но давайте всё же разберём, почему именно так.
Для начала нужно — где хранятся сами методы структур и классов? Они хранятся в метаданных. Получается, делегат будет выделять место в куче, где будет просто ссылка на указанный метод?
Почти Верно!

Но тогда как делегат узнаёт значение Num нашего класса? Всё просто, под капотом в делегате еще и будет храниться ссылка на наш класс => на все его переменные. Именно поэтому наш делегат будет знать значение, он использует не просто функцию, определенную в метаданных, но и еще ссылку на экземпляр класса этой фукнции. А наши экземпляра располагаются в хипе. Данные к ним у нас будут, поэтому все хорошо.

Как это выглядит в Low-level c#? У нас также есть тут ссылка

public static void Main()   {     Counter cnt = new Counter();     new Action((object) cnt, __methodptr(Iteration))();     Console.WriteLine(string.Concat(cnt.Num.ToString(), " Из Main"));   } 

Но что произойдет, если наш класс счетчика станет структурой?

Вывод будет:

1 Из Iteration
0 Из Main

Под капотом, мы будем боксить нашу структуру, копировать ее значение. Структура, которая фактически принимается делегатом и структура cnt — разные обьекты.

Поэтому, если мы сделаем следующее:

public static void Main()     {         Counter cnt = new Counter();         Action act = cnt.Iteration;         act();         Action act2 = act;         act2();         Console.WriteLine(cnt.Num + " Из Main");     }

Вывод будет:

1 Из Iteration
2 Из Iteration
0 Из Main

Структура уже будет в хипе, новый делегат будет указывать уже на структуру в хипе, а не просто копировать ее. Но эти действия никак не влияют на нашу переменную cnt.

Теперь давайте поговорим про замыкания

public static void Main()     {         Counter cnt = new Counter();         Action act =()=>         {             cnt.Iteration();         };         cnt.Iteration();         act();                  Console.WriteLine(cnt.Num + " Из Main");     }

Вот пример кода, как мы видим, у нас есть СТРУКТУРА Counter, все также, но мы используем лямбда функцию и уже внутри функции вызываем у переменной cnt метод.

Вывод:

1 Из Iteration
2 Из Iteration
2 Из Main

Low-level c# код:

using System; using System.Runtime.CompilerServices;  internal class Programm {   public static void Main()   {     Programm.<>c__DisplayClass0_0 cDisplayClass00 = new Programm.<>c__DisplayClass0_0();     cDisplayClass00.cnt = new Counter();     Action act = new Action((object) cDisplayClass00, __methodptr(<Main>b__0));     cDisplayClass00.cnt.Iteration();     act();     Console.WriteLine(string.Concat(cDisplayClass00.cnt.Num.ToString(), " Из Main"));   }    public Programm()   {     base..ctor();   }    [CompilerGenerated]   private sealed class <>c__DisplayClass0_0   {     public Counter cnt;      public <>c__DisplayClass0_0()     {       base..ctor();     }      internal void <Main>b__0()     {       this.cnt.Iteration();     }   } } 

У нас наша лямбда-функция преобразуется в полноценный класс, в котором и создается наша переменная cnt, пусть это и структура, но она часть класса => хранится в хипе, лямбда-функция преобразуется в отдельный метод для созданного компилятором класса. Все действия которые мы совершаем вручную с cnt на самом деле происходят с полем созданного компилятором класса. Именно поэтому у нас происходит работа с одной и той же структурой Counter.

Интересный пример, где эти знания пригодятся — довольно популярный «квиз»:

static void Main(string[] args)     {         Action act = null;         for (int i = 0; i < 10; i++)         {             act += () => { Console.WriteLine(i); };                      }         act();     }

На первый взгляд — просится ответ 0, 1, 2, 3 …

Но давайте применим уже имеющиеся знания — у нас есть делегат Action, к нему каждый раз «прибавляется» одна и та же функция — вывод i, наша лямбда-функция должна преобразоваться в класс, в котором будет поле i (именно её мы и используем в цикле), а также в классе будет присутствовать функция, выводящая на экран значение i. Посмотрим на вывод:

10
10
10
10
10
10
10
10
10
10

namespace ConsoleApp1 {   internal class Program   {     [NullableContext(1)]     private static void Main(string[] args)     {       Action act = (Action) null;       Program.<>c__DisplayClass0_0 cDisplayClass00 = new Program.<>c__DisplayClass0_0();       for (cDisplayClass00.i = 0; cDisplayClass00.i < 10; cDisplayClass00.i++)         act = (Action) Delegate.Combine((Delegate) act, (Delegate) new Action((object) cDisplayClass00, __methodptr(<Main>b__0)));       act();     }      public Program()     {       base..ctor();     }      [CompilerGenerated]     private sealed class <>c__DisplayClass0_0     {       public int i; // эту переменную дергаем в цикле и лямбда-функции        public <>c__DisplayClass0_0()       {         base..ctor();       }        internal void <Main>b__0()       {         Console.WriteLine(this.i); // тут наша фукнция с выводом       }     }   } } 

Будут везде десятки, так как наша переменная i — часть класса => находится в хипе, общая для всех делегатов, а исполнение наших делегатов начинается после цикла for. Получается, когда делегат начинает своё выполнение он работает с переменной i, которая после «прокрутки» цикла for — принимает значение 10.

Что насчет операций «вычитания», «сложения» делегатов? Фактически мы уже увидели выше, что происходит — мы создаем НОВЫЙ делегат, который хранит ссылки на обьекты и методы всех функций, которые мы ему скормили.

Например:

class Program {     static void Main(string[] args)     {         var cnter = new Counter();         Action act = cnter.Iteration;         act += cnter.Iteration;         act();         act += Console.WriteLine;      } }

На IL:

internal class Program   {     [NullableContext(1)]     private static void Main(string[] args)     {       Counter cnter = new Counter();       Action act1 = (Action) Delegate.Combine((Delegate) new Action((object) cnter, __methodptr(Iteration)), (Delegate) new Action((object) cnter, __methodptr(Iteration)));       act1();       Action act2 = (Action) Delegate.Combine((Delegate) act1, (Delegate) (Program.<>O.<0>__WriteLine ?? (Program.<>O.<0>__WriteLine = new Action((object) null, __methodptr(WriteLine)))));     }      public Program()     {       base..ctor();     }      [CompilerGenerated]     private static class <>O     {       public static Action <0>__WriteLine;     }   }

Мы видим — передаем ссылку на объект + ссылка на метод объекта. Но что будет, если счетчик будет структурой?:

class Program {     static void Main(string[] args)     {         var cnter = new Counter();         Action act = cnter.Iteration;         act += cnter.Iteration;         act += act;         act();         Console.WriteLine(cnter.Num);     } }

IL код:

internal class Program   {     [NullableContext(1)]     private static void Main(string[] args)     {       Counter cnter = new Counter();       Action act = (Action) Delegate.Combine((Delegate) new Action((object) cnter, __methodptr(Iteration)), (Delegate) new Action((object) cnter, __methodptr(Iteration)));       ((Action) Delegate.Combine((Delegate) act, (Delegate) act))();       Console.WriteLine(cnter.Num);     }      public Program()     {       base..ctor();     }   }

Вывод будет :1 Из Iteration
1 Из Iteration
2 Из Iteration
2 Из Iteration
0

Почему же так?
Сперва Action act = cnter.Iteration; создается копия структуры

Далее создается ЕЩЕ одна копия структуры, новый делегат хранит ссылки на 2 разных экземпляра структуры.

А после — мы под капотом храним ссылки на уже готовый делегат. То есть мы снова не создаем объекты. Уже храним ссылки на готовый делегат. Поэтому мы будем работать только с 2 экземплярами нашей структуры. В общем — не путаем функции, на которые мы только-только ссылаем делегаты с уже существующим делегатом.

На этом всё, главное — не забывать о существовании структур и как они передаются!!!


ссылка на оригинал статьи https://habr.com/ru/articles/882676/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *