Делегирование - одна из запутывающих концепций в C #, тем более что у вас есть события и обработчики событий, и неясно, как все они связаны. И у нас есть простые делегаты и делегаты многоадресной рассылки.

Давайте сначала поговорим о простой функции делегирования. Вы можете думать о простых делегатах, таких как указатели на функции в C ++ или интерфейсы, содержащие только один метод в Java. Вы назначаете им методы, сигнатуры которых соответствуют явно определенным в определении делегата. Затем вы можете использовать их как ссылки на эти методы. Они также содержат дополнительные материалы, связанные с запуском назначенного метода и сбором информации о назначенном методе. Но почему бы просто не использовать интерфейсы с одним указателем на метод или функцию?

Вам нужно понять этимологию этого слова. Название «делегат» происходит от концепции делегирования в шаблоне разработки стратегии. У вас есть несколько функций (с одинаковой сигнатурой), которые выполняют одну и ту же работу или выполняют одно и то же поведение, и вы делегируете задание / поведение любому из этих методов.

Вот базовый (но нереалистичный) пример. Допустим, нашему коду необходимо извлечь файлы из ZIP-файла в целевой каталог. Допустим, у нас есть собственный метод распаковки архива Windows из системной библиотеки, оболочка приложения Winrar и 7-zip SDK.

Здесь мы сначала определяем тип делегата. Это тоже вызывает недоумение. Чтобы определить ссылку на делегат, нам сначала нужно определить тип делегата для сигнатуры интересующих нас методов. В приведенном выше коде мы говорим, что нас интересуют методы возврата void, которые принимают две строки в качестве аргументов и называют такие методы «UnzipFileDelegate» s.

Затем мы используем этот тип делегата в нашем классе. Обратите внимание, что типы делегатов можно использовать как свойства. Фактически, их можно использовать так же, как и любой другой ссылочный тип, в полях, списках того же типа и т. Д.

Здесь мы делегируем работу по разархивированию файлов методу UnzipFileUsingWindows в классе WindowsZipTools. Мы назначили его в определении класса, но мы могли бы назначить его и во время выполнения.

Например, мы можем позволить пользователю выбрать любой метод для использования из поля со списком в форме.

Когда пользователь выбирает «этот метод назначается новым делегатом для разархивирования файлов» или «задание по разархивированию файлов делегировано к этому методу ».

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

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

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

или вы можете распечатать прогресс в консольном приложении:

или вы можете захотеть зарегистрировать это:

Вы уловили идею.

Итак, что, если мы хотим обновить индикатор выполнения в форме, записать в консоль и перетащить строку в журнал… все сразу?

Мы можем объединить все три функции в метод и передать этот метод в качестве обратного вызова:

Что, если мы хотим включить / отключить ведение журнала во время выполнения? Может быть, мы можем добавить выключатель снаружи?

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

Вот здесь и пригодится шаблон проектирования Observer. С шаблоном Observer мы можем публиковать новое значение прогресса каждый раз, когда оно обновляется, и все подписчики будут уведомлены. Это можно сделать с помощью делегатов многоадресной рассылки. И большую часть времени ключевое слово event и многоадресный делегат EventHandler используются для нашего варианта использования.

Здесь мы можем увидеть, как определяется «обработчик событий». В отличие от простого делегата, которому мы передаем информацию в качестве аргумента, мы обычно определяем новый класс с именем EventNameEventArgs. Затем мы определяем событие, используя ключевое слово event и делегат EventHandler<TEventArgs>, где тип TEventArgs - это наш новый информационный класс. В отличие от шаблона стратегии, обработчики событий являются делегатами многоадресной рассылки, которые используют шаблон наблюдателя и привлекают нескольких подписчиков.

Делегат обработчика событий имеет форму void OnEvent(object sender, TEventArgs e), и мы можем вызывать их всю группу точно так же, как мы это делаем в шаблоне стратегии. Однако обычно мы делаем это, сначала выполняя нулевую проверку, а затем используя метод Invoke. Это связано с тем, что в шаблоне стратегии мы обычно назначаем метод делегату перед его использованием, но в шаблоне наблюдателя другие разработчики, которые используют наш класс, могут отказаться от обработки различных событий. Фактически, вы также можете использовать OnEvent?.Invoke() в шаблоне стратегии, на всякий случай. Без проверки на нуль, если вы попытаетесь вызвать OnProgressChanged(), вы получите NullReferenceException.

В качестве примечания ProgressChangedEventArgs должен унаследовать System.EventArgs от старых версий .NET Framework.

Теперь давайте определим обработчики по сигнатуре void OnEvent(object sender, TEventArgs e).

И то, как вы назначаете обработчики событий многоадресному делегату, отличается. Вместо того, чтобы назначать делегата для замены старого, мы теперь добавляем делегата в группу. Поэтому мы используем += вместо =.

Теперь, когда вызывается OnProgressChanged, оба метода UpdateWorkProgressBar и PrintProgress будут вызываться соответственно. На жаргоне C # это называется «возбуждением события».

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

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

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

Конечно, вам, возможно, не придется передавать какое-то «состояние» обработчикам (наблюдателям) события. Или вы можете просто «уведомить» наблюдателей, чтобы они могли «подтянуть» состояние. В этом случае вам не нужно создавать новый класс EventArgs, и вы будете использовать делегат EventHandler с классом EventArgs по умолчанию:

И поднимите событие:

И, наконец, вы можете написать обработчик:

Здесь параметры не передаются с EventArgs e. Если метод-обработчик хочет проверить состояние различных вещей, которые его интересуют, он может преобразовать sender и извлечь их непосредственно из экземпляра класса.

Так реализованы события в Windows Forms. Это делегаты многоадресной рассылки, которые реализуют шаблон наблюдателя, как и любой другой популярный фреймворк пользовательского интерфейса.

Наконец, ключевое слово event не обязательно для обработчиков событий. Я считаю, что это палка о двух концах. Это делает делегаты событий вызываемыми только из экземпляров класса, в котором они определены. Конечно, мы не хотим, чтобы другие классы вызывали события нашего класса, это нарушает принцип единой ответственности. С ключевым словом event, если кто-то пытается вызвать событие вне класса:

Они получат эту ошибку во время компиляции:

The event 'MyClass.OnEventHappened' can only appear on the left hand side of += or -= (except when used from within the type 'MyClass')

Однако ключевое слово event работает так же, как ключевое слово private, и подклассы MyClass также не могут вызывать OnEventHandler.

Это даст вам ту же ошибку времени компиляции, что и в предыдущем примере.

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

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

Это все, что я хотел рассказать о делегатах и ​​событиях в C #. Я напишу подходящую вступительную статью к этой серии, объясняя, почему я хотел написать о C #. Я узнал подробности из документации Microsoft, различных вопросов на StackOverflow.com и этой замечательной статьи из книги C # In Depth, написанной энтузиастом C # Джоном Скитом. Серьезно, я рекомендую вам прочитать его статью, если вы хотите более подробные сведения :)