четверг, 30 декабря 2021 г.

Делегаты и Лямбда выражения в C# .Net — Шпаргалка или коротко о главном

 

Делегаты и Лямбда выражения в C# .Net — Шпаргалка или коротко о главном

.NET *C# *
Из песочницы

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


Почти все кто мало-мальски работал в .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-operator) или лямбда-выражением (lambda-expression). Так как в целом это одно и то же, то далее по тексту я буду их просто называть «лямбды» в местах, где не нужно подчеркивать их различия.
Стоит упомянуть, что они были введены в C# 3.0, а до них существовали анонимные-функции появившиеся в C# 2.0.

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

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

delegate string MyDeleg (string verb);

Тогда общий синтаксис лямбда-оператора будет следующим:

MyDeleg myDeleg = (string x) => { return x; };

Это именно Лямбда-оператор так как мы обрамляем его тело в фигурные скобки, что позволяет нам поместить в него более одного оператора:

MyDeleg myDeleg = (string x) => { var z = x + x; return z; };

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

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

В случае если имеется лишь один аргумент то можно опустить обрамляющие его скобки:

MyDeleg myDeleg = x => { return x; };

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

AnotherDeleg myDeleg = () => { return x; };


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

— можно опустить фигурные скобки, обрамляющие тело лямбды;

— без вышеупомянутых фигурных скобок нам не нужно использовать ключевое слово return перед оператором и точку запятой после оператора в теле лямбды:

В итоге код определения лямбды может стать крошечным:
MyDeleg myDeleg = x => x+x;


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


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

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

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

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

В итоге


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

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

Методы класса в C#

 


Общие сведения о методах C#

Метод – это набор инструкций, объединенных в блок кода. Методы в C# могут объявляться только в классе или в структуре.

Методы в C# характеризуются своими сигнатурами. Сигнатура метода – совокупность модификаторов доступа метода, других модификаторов метода, типа возвращаемого значения метода, имени метода и всех параметров метода.

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

Если у метода нет параметров, скобки остаются пустыми.

Методы в C# состоят из сигнатуры и тела метода – набора инструкций метода. 

В общем случае метод в C# может быть записан следующим образом:

  1. модификатор_доступа модификатор_метода тип_возвращаемого_значения имя_метода ( тип_параметра_1 параметр_метода_1, тип_параметра_1 параметр_метода_2, ... )
  2. { /* инструкции_метода */ }

В следующем примере класс ConsoleEx содержит 3 метода – WriteEmptyLine()WriteRedLine( string line )WriteGreenLine( string line ):

  1. public class ConsoleEx {
  2. public void WriteEmptyLine() {
  3. Console.WriteLine();
  4. }
  5. public void WriteRedLine( string line ) {
  6. ConsoleColor color = Console.ForegroundColor;
  7. Console.ForegroundColor = ConsoleColor.Red;
  8. Console.WriteLine( line );
  9. Console.ForegroundColor = color;
  10. }
  11. public void WriteGreenLine( string line ) {
  12. ConsoleColor color = Console.ForegroundColor;
  13. Console.ForegroundColor = ConsoleColor.Green;
  14. Console.WriteLine( line );
  15. Console.ForegroundColor = color;
  16. }
  17. }

Сигнатуры этих методов:

  1. public void WriteEmptyLine();
  2. public void WriteRedLine( string line );
  3. public void WriteGreenLine( string line );

Параметры метода

Сигнатура метода содержит полный набор параметров метода – это значения, которые должны передаваться методу вызывающим кодом. Метод может содержать ноль или более параметров. Если метод содержит больше одного параметра, то параметры должны разделяться запятыми. Следующий метод содержит 2 параметра:

  1. public void WriteLine( string line, ConsoleColor lineColor ) {
  2. ConsoleColor color = Console.ForegroundColor;
  3. Console.ForegroundColor = lineColor;
  4. Console.WriteLine( line );
  5. Console.ForegroundColor = color;
  6. }

Вызов метода и аргументы метода

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

В следующем коде вызывается метод с двумя аргументами:

  1. public class Program {
  2. static void Main( string[] args ) {
  3. ConsoleEx console = new ConsoleEx();
  4. // вызов метода ConsoleEx.WriteLine
  5. console.WriteLine(
  6. "Вызов метода с агрументами", // первый аргумент
  7. ConsoleColor.Yellow // второй аргумент
  8. );
  9. Console.ReadKey();
  10. }
  11. }

Если у метода отсутствуют параметры, при вызове метода должны указываться пустые скобки после имени метода:

  1. Сonsole.WriteLine(); //вызов метода без параметров

Именованные аргументы метода

При вызове метода необходимо соблюдать порядок следования аргументов. 

Однако в C# может применяться передача методу именованных аргументов. 

Именованные аргументы – это аргументы метода с указанием названия параметров метода, которым они соответствуют.

В следующем примере вызывается метод с двумя именованными аргументами:

  1. public class Program
  2. {
  3. static void Main( string[] args )
  4. {
  5. ConsoleEx console = new ConsoleEx();
  6. // вызов метода ConsoleEx.WriteLine с именованными аргументами
  7. console.WriteLine(
  8. lineColor: ConsoleColor.Yellow,
  9. line: "Вызов метода с агрументами"
  10. );
  11. Console.ReadKey();
  12. }
  13. }

Обратите внимание, что в этом фрагменте кода аргументы указаны в порядке, отличном от того в котором следуют параметры метода.

Использование позиционных аргументов после именованных в C# не допускается. 

Следующий код вызывает ошибку, не смотря на то, что аргументы указаны в том порядке, в котором представлены параметры метода:

  1. public class Program {
  2. static void Main( string[] args ) {
  3. ConsoleEx console = new ConsoleEx();
  4. console.WriteLine(
  5. line: "Вызов метода с агрументами",
  6. ConsoleColor.Yellow // ошибка !!!
  7. );
  8. Console.ReadKey();
  9. }
  10. }

Необязательные аргументы метода

Необязательные аргументы метода могут использоваться если вычисление тела метода возможно без использования этих аргументов. 

Для определения необязательного параметра в сигнатуре метода после объявления его параметра нужно указать значение этого параметра по умолчанию. 

В следующем фрагменте кода объявляется метод WriteLine с одним обязательным аргументом line и двумя необязательными аргументами – lineColor и backColor:

  1. public void WriteLine(
  2. string line,
  3. ConsoleColor lineColor = ConsoleColor.White,
  4. ConsoleColor backColor = ConsoleColor.Black ) {
  5. ConsoleColor foregroundColor = Console.ForegroundColor;
  6. ConsoleColor backgroundColor = Console.BackgroundColor;
  7. Console.ForegroundColor = lineColor;
  8. Console.BackgroundColor = backColor;
  9. Console.WriteLine( line );
  10. Console.ForegroundColor = foregroundColor;
  11. Console.BackgroundColor = backColor;
  12. }

Необязательные параметры должны следовать после всех обязательных параметров метода.


Возврат значения из метода. Ключевое слово return

Методы в C# могут возвращать значения. Если метод возвращает какое-либо значение, то тип этого значения должен быть указан перед именем метода. При этом все ветви кода метода должны либо возвращать совместимое значение явно при помощи оператора return, либо генерировать исключение при помощи оператора throw. Следующий фрагмент кода содержит метод Sum, возвращающий числовое значение:

  1. public int Sum( int i1, int i2 ) {
  2. return i1 + i2;
  3. }

Мы могли бы не пользоваться в этом методе оператором return, сгенерировав исключение:

  1. public int Sum( int i1, int i2 ) {
  2. throw new Exception();
  3. }

Инструкция return немедленно завершает выполнение метода. Все операторы, которые находятся после инструкции return пропускаются.


Метод Main()

Особым методом в C# является метод Main(), служащий точкой входа в программу. Выполнение программы на языке C# начинается именно с этого метода. Этот метод может получать и обрабатывать аргументы, переданные программе извне. Метод Main() обязательно должен быть помечен специальным модификатором – static. Подробнее об этом методе можно почитать в статье – структура программы C#.


Паттерн 'Репозиторий' в ASP.NET

  Последнее обновление: 1.11.2015         Одним из наиболее часто используемых паттернов при работе с данными является паттерн 'Репозито...