• Microsoft .NET
  • C#.NET
  • Возбуждение событий, обработчики событий и использование делегатов

Возбуждение событий, обработчики событий и использование делегатов - Что такое делегат

ОГЛАВЛЕНИЕ

Так что такое делегат?

Делегат является классом (использующим ключевое слово delegate), способным хранить ссылку на метод. В отличие от других классов, класс делегата имеет сигнатуру и может хранить ссылки на методы, соответствующие его сигнатуре. Делегат эквивалентен безопасному по типу указателю функции или обратному вызову. Хотя делегаты имеют много иных применений, в данном разделе разбирается функционал обработки событий делегатов. Объявления делегата достаточно для определения класса делегата. Объявление задает сигнатуру делегата, а CLR обеспечивает реализацию. Следующий пример показывает объявление делегата события:

public delegate void AlarmEventHandler(object sender, EventArgs e);

Стандартная сигнатура делегата обработчика события определяет метод, не возвращающий значение, первый параметр которого имеет тип Object(объект) и ссылается на экземпляр, возбуждающий событие, а второй параметр которого получен от типа EventArgs и хранит данные события. Делегат в C# похож на указатель функции в C или C++. Использование делегата позволяет помещать ссылку на метод в объект делегата. Затем объект делегата передается коду, вызывающему метод, на который ссылается делегат (если параметры совпадают), без необходимости знать во время компиляции, какой метод будет вызван. В отличие от указателей функций в C или C++, делегаты привязаны к объекту, безопасны по типу и защищены. Как и CLR (общеязыковая среда исполнения) не должна знать, какой язык .NET скомпилирован, CLR знает, что управляемый код порождает метаданные и код на промежуточном языке. Таблицы метаданных проверяются для выполнения безопасной по типу проверки, обеспечивающей передачу нужных данных нужному методу, так как промежуточный код является своевременным (JIT). Короче, объявление делегата определяет тип, инкапсулирующий метод с определенным набором аргументов и тип возвращаемой переменной. Для статических методов объект делегата инкапсулирует метод, который будет вызываться. Для методов экземпляра объект делегата инкапсулирует экземпляр и метод, основанный на методах. Статический метод и метод экземпляра различаются тем, что первый действует на сам тип, а второй действует на экземпляр типа. Если вы имеете объект делегата и подходящий набор аргументов, то можете вызвать делегат с аргументами.

Изучите следующий код:

using System;

// Набор классов для управления книжным магазином:
namespace Bookstore
{
   using System.Collections;

   // Описывает книгу в списке книг:
   public struct Book
   {
      public string Title;        // Название книги.
      public string Author;       // Автор книги.
      public decimal Price;       // Цена книги.
      public bool Paperback;      // Книга в мягкой обложке?
      public Book(string title, string author, decimal price, bool paperBack)
      {
         Title = title;
         Author = author;
         Price = price;
         Paperback = paperBack;
      }
   }

   // Объявляется тип делегата для обработки книги:
   public delegate void ProcessBookDelegate(Book book);

   // Обслуживает базу данных книг.
   public class BookDB
   {
      // Список всех книг в базе данных:
      ArrayList list = new ArrayList();  

      // Добавляет книгу в базу данных:
      public void AddBook(string title, string author, decimal price, bool paperBack)
      {
         list.Add(new Book(title, author, price, paperBack));
      }

      // Вызывает переданный делегат для обработки каждой книги в бумажной обложке:
      public void ProcessPaperbackBooks(ProcessBookDelegate processBook)
      {
         foreach (Book b in list)
         {
            if (b.Paperback)
            // Вызов делегата:
               processBook(b);
         }
      }
   }
}

// Использование классов книжного магазина:
namespace BookTestClient
{
   using Bookstore;

   // Класс для суммирования цен книг и вычисления средней цены на книгу:
   class PriceTotaller
   {
      int countBooks = 0;
      decimal priceBooks = 0.0m;

      internal void AddBookToTotal(Book book)
      {
         countBooks += 1;
         priceBooks += book.Price;
      }

      internal decimal AveragePrice()
      {
         return priceBooks / countBooks;
      }
   }

   // Класс для тестирования базы данных книг:
   class Test
   {
      // Выводит название книги.
      static void PrintTitle(Book b)
      {
         Console.WriteLine("   {0}", b.Title);
      }

      // Здесь начинается выполнение.
      static void Main()
      {
         BookDB bookDB = new BookDB();

         // Инициализирует базу данных несколькими книгами:
         AddBooks(bookDB);     

         // Выводит все названия книг в мягкой обложке:
         Console.WriteLine("Paperback Book Titles:");
         // Создает новый объект делегата, связанный со статическим
         // методом Test.PrintTitle:
         bookDB.ProcessPaperbackBooks(new ProcessBookDelegate(PrintTitle));

         // Получает среднюю цену книги в мягкой обложке с помощью
         // объекта PriceTotaller:
         PriceTotaller totaller = new PriceTotaller();
         // Создает новый объект делегата, связанный с нестатическим
         // методом AddBookToTotal объекта сумматора:
         bookDB.ProcessPaperbackBooks(new ProcessBookDelegate(totaller.AddBookToTotal));
         Console.WriteLine("Average Paperback Book Price: ${0:#.##}",
            totaller.AveragePrice());
      }

      // Инициализирует базу данных книг несколькими тестовыми книгами:
      static void AddBooks(BookDB bookDB)
      {
         bookDB.AddBook("Язык программирования C",
            "Брайан У. Керниган и Дэннис М. Ричи", 19.95m, true);
         bookDB.AddBook("Стандарт Юникод 2.0",
            "Консорциум Юникода", 39.95m, true);
         bookDB.AddBook("Энциклопедия MS-DOS",
            "Ray Duncan", 129.95m, false);
         bookDB.AddBook("Подсказки Догберта для тупых",
            "Скотт Адамс", 12.00m, true);
      }
   }
}

Теперь изучите вывод:

Названия книг в мягкой обложке:   
Язык программирования C
   Стандарт Юникод 2.0
   Подсказки Догберта для тупых
Средняя цена книги в мягкой обложке: $23.97

Был создан экземпляр делегата, так как раз делегат был объявлен, то надо создать объект делегата и связать его с конкретным методом. Как и все прочие объекты, новый объект делегата создается с помощью выражения new (новый). В этом заключается принципиальное различие между объектно-ориентированным и процедурным языком C: в C, при объявлении переменной компилятор оповещается о типе данных, который будет служить в качестве переменной, имеющей присвоенное значение. Затем компилятор выделяет память, требуемую для данного конкретного типа данных. Однако при создании делегата выражению new передается особый аргумент, записанный как вызов метода, но без аргументов для метода, как показано в следующем операторе:

bookDB.ProcessPaperbackBooks(new ProcessBookDelegate(PrintTitle));

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

bookDB.ProcessPaperbackBooks(new ProcessBookDelegate(totaller.AddBookToTotal));

создает новый объект делегата, связанный с нестатическим методом AddBookToTotal объекта totaller. В обоих случаях новый объект делегата немедленно передается методу ProcessPaperbackBooks. После создания делегата связанный с ним метод никогда не меняется — объекты делегатов неизменяемые.

Реагирование на событие

Скачиваемый файл SystemTimer является приложением Windows Forms, содержащим управляющий элемент «индикатор выполнения». Идея – управлять управляющим элементом «индикатор выполнения» путем реагирования на события таймера. Объекты таймера могут применяться для генерации событий через заданное число миллисекунд. Вы просто создаете проект Windows Forms, перетаскиваете управляющий элемент «индикатор выполнения» на плоскость формы, а затем объявляете объект Timer. Загрузите код в папку по имени TimerEvents в папке Projects вашей версии Visual Studio. Не пытайтесь запустить файл решения из Zip-архива. Извлеките файлы в указанную подпапку папки Projects и дважды щелкните по файлу решения, чтобы увидеть, как индикатор выполнения реагируют после истечения определенного числа миллисекунд.

Ниже приведен пример кода из книги Джеффри Ричтера “CLR через C#”, поясняющий сущность делегатов:

using System;
using System.Windows.Forms;
using System.IO;

// Объявлен тип делегата; экземпляры ссылаются на метод,
// принимающий параметр Int32 и возвращающий void (пустой тип).
internal delegate void Feedback(Int32 value);

public sealed class Program {
   public static void Main() {
      StaticDelegateDemo();
      InstanceDelegateDemo();
      ChainDelegateDemo1(new Program());
      ChainDelegateDemo2(new Program());
   }

   private static void StaticDelegateDemo() {
      Console.WriteLine("----- Пример статического делегата -----");
      Counter(1, 3, null);
      Counter(1, 3, new Feedback(Program.FeedbackToConsole));
      Counter(1, 3, new Feedback(FeedbackToMsgBox)); // "Программа." необязателен
      Console.WriteLine();
   }

   private static void InstanceDelegateDemo() {
      Console.WriteLine("----- Пример делегата экземпляра -----");
      Program p = new Program();
      Counter(1, 3, new Feedback(p.FeedbackToFile));

      Console.WriteLine();
   }

   private static void ChainDelegateDemo1(Program p) {
      Console.WriteLine("----- Пример делегата-цепи 1 -----");
      Feedback fb1 = new Feedback(FeedbackToConsole);
      Feedback fb2 = new Feedback(FeedbackToMsgBox);
      Feedback fb3 = new Feedback(p.FeedbackToFile);

      Feedback fbChain = null;
      fbChain = (Feedback) Delegate.Combine(fbChain, fb1);
      fbChain = (Feedback) Delegate.Combine(fbChain, fb2);
      fbChain = (Feedback) Delegate.Combine(fbChain, fb3);
      Counter(1, 2, fbChain);

      Console.WriteLine();
      fbChain = (Feedback) Delegate.Remove(fbChain,
                           new Feedback(FeedbackToMsgBox));
      Counter(1, 2, fbChain);
   }

   private static void ChainDelegateDemo2(Program p) {
      Console.WriteLine("----- Пример делегата-цепи 2 -----");
      Feedback fb1 = new Feedback(FeedbackToConsole);
      Feedback fb2 = new Feedback(FeedbackToMsgBox);
      Feedback fb3 = new Feedback(p.FeedbackToFile);

      Feedback fbChain = null;
      fbChain += fb1;
      fbChain += fb2;
      fbChain += fb3;
      Counter(1, 2, fbChain);

      Console.WriteLine();
      fbChain -= new Feedback(FeedbackToMsgBox);
      Counter(1, 2, fbChain);
   }
  
   private static void Counter(Int32 from, Int32 to, Feedback fb) {
      for (Int32 val = from; val <= to; val++) {
         // Если какие-либо обратные вызовы заданы, вызывает их
         if (fb != null)
            fb(val);
      }
   }

   private static void FeedbackToConsole(Int32 value) {
      Console.WriteLine("Item=" + value);
   }

   private static void FeedbackToMsgBox(Int32 value) {
      MessageBox.Show("Item=" + value);
   }

   private void FeedbackToFile(Int32 value) {
      StreamWriter sw = new StreamWriter("Status", true);
      sw.WriteLine("Item=" + value);
      sw.Close();
   }
}

При компиляции и выполнении данного кода заметьте, что командная строка отображает строки, соответствующие кнопкам Windows Forms. Теперь вспомните, что делегат обозначает сигнатуру метода обратного вызова. Код начинается с объявления внутреннего делегата, Feedback (обратная связь). Feedback обозначает метод, принимающий параметр (Int32) и возвращающий void. Теперь класс Program определяет закрытый статический метод по имени Counter (счетчик). Этот метод считает целые числа от аргумента from до аргумента to. Counter также принимает fb, являющийся ссылкой на объект делегата Feedback. CLR требует создавать любой объект путем вызова оператора new. Например, объект Patient(пациент) создавался бы так:

Patient p = new Patient();

Оператор new выполняет ряд скрытых операций, не считая использование переменной (в данном случае p) в качестве ссылки, где хранятся параметры конструктора. Counter проходит в цикле по всем целым числам, и для каждого целого числа, если переменная fb не null (пустая), вызывается метод обратного вызова (заданный переменной fb). Этому методу обратного вызова передается значение обрабатываемого элемента и номер элемента. Метод обратного вызова может быть разработан и реализован для обработки каждого элемента любым нужным образом.