Справочник по C# Герберт Шилдт ббк 32. 973. 26018 75 Ш57 удк 681 07 Издательский дом "Вильямс" Зав редакцией
Скачать 5.05 Mb.
|
Глава 15 Делегаты и события 410 Часть I. Язык C# этой главе рассматриваются два новых C#-средства: делегаты и события. Делегат предоставляет возможность инкапсулировать метод, а событие — это своего рода уведомление о том, что имело место некоторое действие. Делегаты и события связанны между собой, поскольку событие создается на основе делегата. Эти средства расширяют диапазон задач программирования, к которым можно применить язык C#. Делегаты Начнем с определения термина делегат (delegate). Делегат — это объект, который может ссылаться на метод. Таким образом, создавая делегат, вы по сути создаете объект, который может содержать ссылку на метод. Более того, этот метод можно вызвать посредством соответствующей ссылки. Таким образом, делегат может вызывать метод, на который он ссылается. На первый взгляд идея ссылки на метод может показаться странной, поскольку обычно мы имеем дело с ссылками, которые указывают на объекты, но в действительности здесь разница небольшая. Как разъяснялось выше, ссылка по существу представляет собой адрес памяти. Следовательно, ссылка на объект — это адрес объекта. Даже несмотря на то что метод не является объектом, он тоже имеет отношение к физической области памяти, а адрес его точки входа — это адрес, к которому происходит обращение при вызове метода. Этот адрес можно присвоить делегату. Если уж делегат ссылается на метод, этот метод можно вызвать посредством данного делегата. На заметку Если вы знакомы с C/C++, то вам будет полезно узнать, что делегат в C# аналогичен указателю на функцию в C/C++ Важно понимать, что во время выполнения программы один и тот же делегат можно использовать для вызова различных методов, просто заменив метод, на который ссылается этот делегат. Таким образом, метод, который будет вызван делегатом, определяется не в период компиляции программы, а во время ее работы. В этом и состоит достоинство делегата. Делегат объявляется с помощью ключевого слова delegate . Общая форма объявления делегата имеет следующий вид: delegate тип_возврата имя ( список_параметров ); Здесь элемент тип_возврата представляет собой тип значений, возвращаемых методами, которые этот делегат будет вызывать. Имя делегата указывается элементом имя . Параметры, принимаемые методами, которые вызываются посредством делегата, задаются с помощью элемента список_параметров . Делегат может вызывать только такие методы, у которых тип возвращаемого значения и список параметров (т.е. его сигнатура) совпадают с соответствующими элементами объявления делегата. Делегат может вызывать либо метод экземпляра класса, связанный с объектом, или статический метод, связанный с классом, Чтобы увидеть делегат в действии, начнем со следующего простого примера: // Простой пример использования делегата. using System; // Объявляем делегат. delegate string strMod(string str); class DelegateTest { В Глава 15. Делегаты и события 411 static string replaceSpaces(string a) { Console.WriteLine("Замена пробелов дефисами."); return a.Replace(' ', '-'); } // Метод удаляет пробелы. static string removeSpaces(string a) { string temp = ""; int i; Console.WriteLine("Удаление пробелов."); for(i=0; i < a.Length; i++) if(a[i] != ' ') temp += a[i]; return temp; } // Метод реверсирует строку. static string reverse(string a) { string temp = ""; int i, j; Console.WriteLine("Реверсирование строки."); for(j=0, i=a.Length-1; i >= 0; i--, j++) temp += a[i]; return temp; } public static void Main() { // Создание делегата. strMod strOp = new strMod(replaceSpaces); string str; // Вызываем методы посредством делегата. str = strOp("Это простой тест."); Console.WriteLine("Результирующая строка: " + str); Console.WriteLine(); strOp = new strMod(removeSpaces); str = strOp("Это простой тест."); Console.WriteLine("Результирующая строка: " + str); Console.WriteLine(); strOp = new strMod(reverse); str = strOp("Это простой тест."); Console.WriteLine("Результирующая строка: " + str); } } Результаты выполнения этой программы выглядят так: Замена пробелов дефисами. Результирующая строка: Это-простой-тест. Удаление пробелов. Результирующая строка: Этопростойтест. Реверсирование строки. Результирующая строка: .тсет йотсорп отЭ 412 Часть I. Язык C# Итак, в программе объявляется делегат с именем strMod , который принимает один параметр типа string и возвращает string -значение. В классе DelegateTest объявлены три статических метода, сигнатура которых совпадает с сигнатурой, заданной делегатом. Эти методы предназначены для модификации строк определенного вида. Обратите внимание на то, что метод replaceSpaces() для замены пробелов дефисами использует метод Replace() — один из методов класса string. В методе Main() создается ссылка типа strMod с именем strOp , и ей присваивается ссылка на метод replaceSpaces() . Внимательно рассмотрите следующую строку: strMod strOp = new strMod(replaceSpaces); Обратите внимание на то, что метод replaceSpaces() передается делегату в качестве параметра. Здесь используется только имя метода (параметры не указываются). Это наблюдение можно обобщить: при реализации делегата задается только имя метода, на который должен ссылаться этот делегат. Кроме того, объявление метода должно соответствовать объявлению делегата. В противном случае вы получите сообщение об ошибке еще во время компиляции. Затем метод replaceSpaces() вызывается посредством экземпляра делегата с именем strOp , как показано в следующей строке: str = strOp("Это простой тест."); Поскольку экземпляр strOp ссылается на метод replaceSpaces() , то вызывается именно метод replaceSpaces() . Затем экземпляру делегата strOp присваивается ссылка на метод removeSpaces() , после чего strOp вызывается снова. На этот раз вызывается метод removeSpaces() Наконец, экземпляру делегата strop присваивается ссылка на метод reverse() , и strOp вызывается еще раз. Это, как нетрудно догадаться, приводит к вызову метода reverse(). Главное в этом примере то, что вызов экземпляра делегата strOp трансформируется в обращение к методу, на который ссылается strOp при вызове. Таким образом, решение о вызываемом методе принимается во время выполнения программы, а не в период компиляции. Несмотря на то что в предыдущем примере используются статические методы, делегат может также ссылаться на методы экземпляров класса. Однако он должен при этом использовать объектную ссылку. Например, вот как выглядит предыдущая программа, переписанная с целью инкапсуляции операций над строками внутри класса StringOps : // Делегаты могут ссылаться также на методы // экземпляров класса. using System; // Объявляем делегат. delegate string strMod(string str); class StringOps { // Метод заменяет пробелы дефисами. public string replaceSpaces(string a) { Console.WriteLine("замена пробелов дефисами."); return a.Replace(' ', '-'); } // Метод удаляет пробелы. public string removeSpaces(string a) { string temp = ""; int i; Глава 15. Делегаты и события 413 Console.WriteLine("Удаление пробелов."); for(i=0; i < a.Length; i++) if(a[i] != ' ') temp += a[i]; return temp; } // Метод реверсирует строку. public string reverse(string a) { string temp = ""; int i, j; Console.WriteLine("Реверсирование строки."); for(j=0, i=a.Length-1; i >= 0; i--, j++) temp += a[i]; return temp; } } class DelegateTest { public static void Main() { StringOps so = new StringOps(); // Создаем экземпляр // класса StringOps. // Создаем делегат. strMod strOp = new strMod(so.replaceSpaces); string str; // Вызываем методы с использованием делегатов. str = strOp("Это простой тест."); Console.WriteLine("Результирующая строка: " + str); Console.WriteLine(); strOp = new strMod(so.removeSpaces); str = strOp("Это простой тест."); Console.WriteLine("Результирующая строка: " + str); Console.WriteLine(); strOp = new strMod(so.reverse); str = strOp("Это простой тест."); Console.WriteLine("Результирующая строка: " + str); } } Результаты выполнения этой программы совпадают с результатами предыдущей версии, но в этом случае делегат ссылается на методы экземпляра класса StringOps Многоадресатная передача Одна из самых интересных возможностей делегата — поддержка многоадресатной передачи (multicasting). Выражаясь простым языком, Многоадресатная передача — это способность создавать список вызовов (или цепочку вызовов) методов, которые должны автоматически вызываться при вызове делегата. Такую цепочку создать нетрудно. Достаточно создать экземпляр делегата, а затем для добавления методов в эту цепочку использовать оператор “ += ”. Для удаления метода из цепочки используется оператор “ -= ”. (Можно также для добавления и удаления методов использовать в отдельности операторы “ + ”, “ - “ и “ = ”, но чаще применяются составные операторы " += " и " -= ".) 414 Часть I. Язык C# Делегат с многоадресатной передачей имеет одно ограничение: он должен возвращать тип void Рассмотрим следующий пример многоадресатной передачи. Это — переработанный вариант предыдущих примеров, в котором тип string для значений, возвращаемых методами обработки строк, заменен типом void , а для возврата модифицированных строк используется ref -параметр. // Демонстрация использования многоадресатной передачи. using System; // Объявляем делегат. delegate void strMod(ref string str); class StringOps { // Метод заменяет пробелы дефисами. static void replaceSpaces(ref string a) { Console.WriteLine("Замена пробелов дефисами."); a = a.Replace(' ', '-'); } // Метод удаляет пробелы. static void removeSpaces(ref string a) { string temp = ""; int i; Console.WriteLine("Удаление пробелов."); for(i=0; i < a.Length; i++) if(a[i] != ' ') temp += a[i]; a = temp; } // Метод реверсирует строку. static void reverse(ref string a) { string temp = ""; int i, j; Console.WriteLine("Реверсирование строки."); for(j=0, i=a.Length-1; i >= 0; i--, j++) temp += a[i]; a = temp; } public static void Main() { // Создаем экземпляры делегатов. strMod strOp; strMod replaceSp = new strMod(replaceSpaces); strMod removeSp = new strMod(removeSpaces); strMod reverseStr = new strMod(reverse); string str = "Это простой тест."; // Организация многоадресатной передачи, strOp = replaceSp; strOp += reverseStr; // Вызов делегата с многоадресатной передачей. strOp(ref str); Глава 15. Делегаты и события 415 Console.WriteLine("Результирующая строка: " + str); Console.WriteLine(); // Удаляем метод замены пробелов и // добавляем метод их удаления. strOp -= replaceSp; strOp += removeSp; str = "Это простой тест."; // Восстановление // исходной строки. // Вызов делегата с многоадресатной передачей. strOp(ref str); Console.WriteLine("Результирующая строка: " + str); Console.WriteLine(); } } Вот как выглядят результаты выполнения этой программы: Замена пробелов дефисами. Реверсирование строки. Результирующая строка: .тсет-йотсорп-отЭ Реверсирование строки. Удаление пробелов. Результирующая строка: .тсетйотсорпотЭ В методе Main() создаются четыре экземпляра делегата. Первый, strOp , имеет null -значение. Три других ссылаются на методы модификации строк. Затем организуется делегат для многоадресатной передачи, который вызывает методы removeSpaces() и reverse() . Это достигается благодаря следующим строкам программы: strOp = replaceSp; strOp += reverseStr; Сначала делегату strOp присваивается ссылка replaceSp . Затем, с помощью оператора “ += ”, в цепочку вызовов добавляется ссылка reverseStr . При вызове делегата strOp в этом случае вызываются оба метода, заменяя пробелы дефисами и реверсируя строку. Затем при выполнении строки программы strOp -= replaceSp; из цепочки вызовов удаляется ссылка replaceSp , а с помощью строки strOp += removeSp; в цепочку вызовов добавляется ссылка removeSp Затем делегат strOp вызывается снова. На этот раз из исходной строки удаляются пробелы, после чего она реверсируется. Цепочки вызовов, организованные с помощью делегата, — мощный механизм, который позволяет определять набор методов, выполняемых “единым блоком”. Как будет показано ниже, цепочки делегатов имеют особое значение для событий. Класс System.Delegate Все делегаты представляют собой классы, которые неявным образом выводятся из класса System.Delegate . Обычно его члены не используются напрямую, и в этой книге не показано явное использование класса System.Delegate . Все же в некоторых ситуациях его члены могут оказаться весьма полезными. 416 Часть I. Язык C# Назначение делегатов Несмотря на то что предыдущие примеры программ продемонстрировали, "как" работают делегаты, они не содержали ответа на вопрос “зачем это нужно?”. Так вот, делегаты используются по двум основным причинам. Во-первых, как будет показано в следующем разделе, делегаты обеспечивают поддержку функционирования событий. Во- вторых, делегаты позволяют во время выполнения программы выполнить метод, который точно не известен в период компиляции. Эта возможность особенно полезна, когда нужно создать оболочку, к которой могли бы подключаться программные компоненты. Например, представьте графическую программу (наподобие стандартной утилиты Windows Paint). Используя делегат, можно было бы разрешить пользователю подключать специальные цветные светофильтры или анализаторы изображений. Более того, пользователь мог бы создавать “свои” последовательности этих фильтров или анализаторов. С помощью делегатов организовать такой алгоритм очень легко. События На основе делегатов построено еще одно важное средство C#: событие (event). Событие - это по сути автоматическое уведомление о выполнении некоторого действия. События работают следующим образом. Объект, которому необходима информация о некотором событии, регистрирует обработчик для этого события. Когда ожидаемое событие происходит, вызываются все зарегистрированные обработчики. А теперь внимание: обработчики событий представляются делегатами. События — это члены класса, которые объявляются с использованием ключевого слова event. Наиболее распространенная форма объявления события имеет следующий вид: event событийный_делегат объект ; Здесь элемент событийный_делегат означает имя делегата, используемого для поддержки объявляемого события, а элемент объект — это имя создаваемого событийного объекта. Начнем с рассмотрения очень простого примера. // Демонстрация использования простейшего события. using System; // Объявляем делегат для события. delegate void MyEventHandler(); // Объявляем класс события. class MyEvent { public event MyEventHandler SomeEvent; // Этот метод вызывается для генерирования события. public void OnSomeEvent() { if(SomeEvent != null) SomeEvent(); } } class EventDemo { // Обработчик события. static void handler() { Глава 15. Делегаты и события 417 Console.WriteLine("произошло событие."); } public static void Main() { MyEvent evt = new MyEvent(); // Добавляем метод handler() в список события. evt.SomeEvent += new MyEventHandler(handler); // Генерируем событие. evt.OnSomeEvent(); } } При выполнении программа отображает следующие результаты: Произошло событие. Несмотря на простоту, программа содержит все элементы, необходимые для надлежащей обработки события. Рассмотрим их по порядку. Программа начинается с такого объявления делегата для обработчика события: delegate void MyEventHandler(); Все события активизируются посредством делегата. Следовательно, событийный делегат определяет сигнатуру для события. В данном случае параметры отсутствуют, однако событийные параметры разрешены. Поскольку события обычно предназначены для многоадресатной передачи, они должны возвращать значение типа void Затем создается класс события MyEvent . При выполнении следующей строки кода, принадлежащей этому классу, объявляется событийный объект SomeEvent : public event MyEventHandler SomeEvent; Обратите внимание на синтаксис. Именно так объявляются события всех типов. Кроме того, внутри класса MyEvent объявляется метод OnSomeEvent() , который в этой программе вызывается, чтобы сигнализировать о событии. (Другими словами, этот метод вызывается, когда происходит событие.) Как показано в следующем фрагменте кода, он вызывает обработчик события посредством делегата SomeEvent if(SomeEvent != null) SomeEvent(); Обратите внимание на то, что обработчик события вызывается только в том случае, если делегат SomeEvent не равен null -значению. Поскольку другие части программы, чтобы получить уведомлении о событии, должны зарегистрироваться, можно сделать так, чтобы метод OnSomeEvent() был вызван до регистрации любого обработчика события. Чтобы предотвратить вызов null -объекта, событийный делегат необходимо протестировать и убедиться в том, что он не равен null -значению. Внутри класса EventDemo создается обработчик события handler() . В этом примере обработчик события просто отображает сообщение, но ясно, что другие обработчики могли бы выполнять более полезные действия. Как показано в следующем фрагменте кода, в методе Main() создается объект класса MyEvent , а метод handler() регистрируется в качестве обработчика этого события. MyEvent evt = new MyEvent(); // Добавляем метод handler() в список события. evt.SomeEvent += new MyEventHandler(handler); Обратите внимание на то, что обработчик добавляется в список с использованием составного оператора “ += ”. Следует отметить, что события поддерживают только операторы “ += ” и “ -= ”. В нашем примере метод handler() является статическим, но в 418 Часть I. Язык C# общем случае обработчики событий могут быть методами экземпляров классов. Наконец, при выполнении следующей инструкции “происходит” событие, о котором мы так много говорили. // Генерируем событие. evt.OnSomeEvent(); При вызове метода OnSomeEvent() вызываются все зарегистрированные обработчики событий. В данном случае зарегистрирован только один обработчик, но, как вы увидите в следующем разделе, их могло бы быть и больше. Пример события для многоадресатной передачи Подобно делегатам события могут предназначаться для многоадресатной передачи. В этом случае на одно уведомление о событии может отвечать несколько объектов. Рассмотрим пример. // Демонстрация использования события, предназначенного // для многоадресатной передачи. using System; // Объявляем делегат для события. delegate void MyEventHandler(); // Объявляем класс события. class MyEvent { public event MyEventHandler SomeEvent; // Этот метод вызывается для генерирования события. public void OnSomeEvent() { if(SomeEvent != null) SomeEvent(); } } class X { public void Xhandler() { Console.WriteLine("Событие, полученное объектом X."); } } class Y { public void Yhandler() { Console.WriteLine("Событие, полученное объектом Y."); } } class EventDemo { static void handler() { Console.WriteLine( "Событие, полученное классом EventDemo."); } public static void Main() { MyEvent evt = new MyEvent(); X xOb = new X(); Y yOb = new Y(); Глава 15. Делегаты и события 419 // Добавляем обработчики в список события. evt.SomeEvent += new MyEventHandler(handler); evt.SomeEvent += new MyEventHandler(xOb.Xhandler); evt.SomeEvent += new MyEventHandler(yOb.Yhandler); // Генерируем событие. evt.OnSomeEvent(); Console.WriteLine(); // Удаляем один обработчик. evt.SomeEvent -= new MyEventHandler(xOb.Xhandler); evt.OnSomeEvent(); } } Результаты выполнения этой программы имеют следующий вид: Событие, полученное классом EventDemo. Событие, полученное объектом X. Событие, полученное объектом Y. Событие, полученное классом EventDemo. Событие, полученное объектом Y. В этом примере создается два дополнительных класса X и Y , в которых также определяются обработчики событий, совместимые с сигнатурой делегата MyEventHandler . Следовательно, эти обработчики могут стать частью цепочки событийных вызовов. Обратите внимание на то, что обработчики в классах X и y не являются статическими. Это значит, что сначала должны быть созданы объекты каждого класса, после чего в цепочку событийных вызовов должен быть добавлен обработчик, связанный с каждым экземпляром класса. Различие между статическими обработчиками и обработчиками экземпляров классов рассматривается в следующем разделе. Сравнение методов экземпляров классов со статическими методами, используемыми в качестве обработчиков событий Несмотря на то что и методы экземпляров классов, и статические методы могут служить обработчиками событий, в их использовании в этом качестве есть существенные различия. Если в качестве обработчика используется статический метод, уведомление о событии применяется к классу (и неявно ко всем объектам этого класса). Если же в качестве обработчика событий используется метод экземпляра класса, события посылаются к конкретным экземплярам этого класса. Следовательно, каждый объект класса, который должен получать уведомление о событии, необходимо регистрировать в отдельности. На практике в большинстве случаев “роль” обработчиков событий “играют” методы экземпляров классов, но, безусловно, все зависит от конкретной ситуации. Теперь перейдем к рассмотрению примеров. В следующей программе создается класс X , в котором в качестве обработчика событий определен метод экземпляра. Это значит, что для получения информации о событиях каждый объект класса X необходимо регистрировать отдельно. Для демонстрации этого факта программа готовит уведомление о событии для многоадресатной передачи трем объектам типа X /* При использовании в качестве обработчиков событий методов экземпляров уведомление о событиях принимают отдельные объекты. */ using System; 420 Часть I. Язык C# // Объявляем делегат для события. delegate void MyEventHandler(); // Объявляем класс события. class MyEvent { public event MyEventHandler SomeEvent; // Этот метод вызывается для генерирования события. public void OnSomeEvent() { if(SomeEvent != null) SomeEvent(); } } class X { int id; public X(int x) { id = x; } // Метод экземпляра, используемый в качестве // обработчика событий. public void Xhandler() { Console.WriteLine("Событие принято объектом " + id); } } class EventDemo { public static void Main() { MyEvent evt = new MyEvent(); X o1 = new X(1); X o2 = new X(2); X o3 = new X(3); evt.SomeEvent += new MyEventHandler(o1.Xhandler); evt.SomeEvent += new MyEventHandler(o2.Xhandler); evt.SomeEvent += new MyEventHandler(o3.Xhandler); // Генерируем событие. evt.OnSomeEvent(); } } Результаты выполнения этой программы имеют такой вид: Событие принято объектом 1 Событие принято объектом 2 Событие принято объектом 3 Как подтверждают эти результаты, каждый объект заявляет о своей заинтересованности в событии и получает о нем отдельное уведомление. Если же в качестве обработчика событий используется статический метод, то, как показано в следующей программе, события обрабатываются независимо от объекта. /* При использовании в качестве обработчиков событий статического метода уведомление о событиях получает класс. */ using System; // Объявляем делегат для события. Глава 15. Делегаты и события 421 delegate void MyEventHandler(); // Объявляем класс события. class MyEvent { public event MyEventHandler SomeEvent; // Этот метод вызывается для генерирования события. public void OnSomeEvent() { if(SomeEvent != null) SomeEvent(); } } class X { /* Это статический метод, используемый в качестве обработчика события. */ public static void Xhandler() { Console. WriteLine("Событие получено классом."); } } class EventDemo { public static void Main() { MyEvent evt = new MyEvent(); evt.SomeEvent += new MyEventHandler(X.Xhandler); // Генерируем событие. evt.OnSomeEvent(); } } Вот как выглядят результаты выполнения программы: Событие получено классом. Обратите внимание на то, что в программе не создается ни одного объекта типа X . Но поскольку handler() — статический метод класса X , его можно связать с событием SomeEvent и обеспечить его выполнение при вызове метода OnSomeEvent() Использование событийных средств доступа Предусмотрены две формы записи инструкций, связанных с событиями. Форма, используемая в предыдущих примерах, обеспечивала создание событий, которые автоматически управляют списком вызова обработчиков, включая такие операции, как добавление обработчиков в список и удаление их из списка. Таким образом, можно было не беспокоиться о реализации операций по управлению этим списком. Поэтому такие типы событий, безусловно, являются наиболее применимыми. Однако можно и самим организовать ведение списка обработчиков событий, чтобы, например, реализовать специализированный механизм хранения событий. Чтобы управлять списком обработчиков событий, используйте вторую форму event -инструкции, которая позволяет использовать средства доступа к событиям. Эти средства доступа дают возможность управлять реализацией списка обработчиков событий. Упомянутая форма имеет следующий вид: event событийный_делегат имя_события { add { 422 Часть I. Язык C# // Кол добавления события в цепочку событий. } remove { // Код удаления события из цепочки событий. } } Эта форма включает два средства доступа к событиям: add и remove . Средство доступа add вызывается в случае, когда с помощью оператора “ += ” в цепочку событий добавляется новый обработчик, а средство доступа remove вызывается, когда с помощью оператора “ -= ” из цепочки событий удаляется новый обработчик. Средство доступа add или remove при вызове получает обработчик, который необходимо добавить или удалить, в качестве параметра. Этот параметр, как и в случае использования других средств доступа, называется value . При реализации средств доступа add и remove можно задать собственную схему хранения обработчиков событий. Например, для этого вы могли бы использовать массив, стек или очередь. Рассмотрим пример использования событийных средств доступа. Здесь для хранения обработчиков событий взят массив. Поскольку этот массив содержит три элемента, в любой момент времени в событийной цепочке может храниться только три обработчика событий. // Создание собственных средств управления списком событий. using System; // Объявляем делегат для события. delegate void MyEventHandler(); // Объявляем класс события для хранения трех // обработчиков событий. class MyEvent { MyEventHandler[] evnt = new MyEventHandler[3]; public event MyEventHandler SomeEvent { // Добавляем обработчик события в список. add { int i; for(i=0; i < 3; i++) if(evnt[i] == null) { evnt[i] = value; break; } if(i == 3) Console.WriteLine( "Список обработчиков событий полон."); } // Удаляем обработчик события из списка. remove { int i; for(i=0; i < 3; i++) if(evnt[i] == value) { evnt[i] = null; break; } Глава 15. Делегаты и события 423 if(i == 3) Console.WriteLine("Обработчик события не найден."); } } // Этот метод вызывается для генерирования событий. public void OnSomeEvent() { for(int i=0; i < 3; i++) if(evnt[i] != null) evnt[i](); } } // Создаем классы, которые используют // делегат MyEventHandler. class W { public void Whandler() { Console.WriteLine("Событие получено объектом W."); } } class X { public void Xhandler() { Console.WriteLine("Событие получено объектом X."); } } class Y { public void Yhandler() { Console.WriteLine("Событие получено объектом Y."); } } class Z { public void Zhandler() { Console.WriteLine("Событие получено объектом Z."); } } class EventDemo { public static void Main() { MyEvent evt = new MyEvent(); W wOb = new W(); X xOb = new X(); Y yOb = new Y(); Z zOb = new Z(); // Добавляем обработчики в список. Console.WriteLine("Добавление обработчиков событий."); evt.SomeEvent += new MyEventHandler(wOb.Whandler); evt.SomeEvent += new MyEventHandler(xOb.Xhandler); evt.SomeEvent += new MyEventHandler(yOb.Yhandler); // Этот обработчик сохранить нельзя — список полон. evt.SomeEvent += new MyEventHandler(zOb.Zhandler); Console.WriteLine(); // Генерируем события. 424 Часть I. Язык C# evt.OnSomeEvent(); Console.WriteLine(); // Удаляем обработчик из списка. Console.WriteLine("Удаляем обработчик xOb.Xhandler."); evt.SomeEvent -= new MyEventHandler(xOb.Xhandler); evt.OnSomeEvent(); Console.WriteLine(); // Пытаемся удалить его еще раз. Console.WriteLine( "Попытка повторно удалить обработчик xOb.Xhandler."); evt.SomeEvent -= new MyEventHandler(xOb.Xhandler); evt.OnSomeEvent(); Console.WriteLine(); // Теперь добавляем обработчик Zhandler. Console.WriteLine("Добавляем обработчик zOb.Zhandler."); evt. SomeEvent += new MyEventHandler(zOb. Zhandler); evt.OnSomeEvent(); } } Вот результаты выполнения программы: Добавление обработчиков событий. Список обработчиков событий полон. Событие получено объектом W. Событие получено объектом X. Событие получено объектом Y. Удаляем обработчик xOb.Xhandler. Событие получено объектом W. Событие получено объектом Y. Попытка повторно удалить обработчик xOb.Xhandler. Обработчик события не найден. Событие получено объектом W. Событие получено объектом Y. Добавляем обработчик zOb.Zhandler. Событие получено объектом W. Событие получено объектом Z. Событие получено объектом Y. Рассмотрим внимательно код этой программы. Сначала определяется делегат обработчика события MyEventHandler . Код класса MyEvent , как показано в следующей инструкции, начинается с определения трехэлементного массива обработчиков событий evnt MyEventHandler[] evnt = new MyEventHandler[3]; Этот массив предназначен для хранения обработчиков событий, которые добавлены в цепочку событий. Элементы массива evnt инициализируются null -значениями по умолчанию. Приведем event -инструкцию, в которой используются событийные средства доступа. Глава 15. Делегаты и события 425 public event MyEventHandler SomeEvent { // Добавляем обработчик события в список, add { int i; for(i=0; i < 3; i++) if(evnt[i] == null) { evnt[i] = value; break; } if(i == 3) Console.WriteLine( "Список обработчиков событий полон."); } // Удаляем обработчик события из списка. remove { int i; for(i=0; i < 3; i++) if(evnt[i] == value) { evnt[i] = null; break; } if(i == 3) Console.WriteLine("Обработчик события не найден."); } } При добавлении в список обработчика событий вызывается add -средство, и ссылка на этот обработчик (содержащаяся в параметре value ) помешается в первый встретившийся неиспользуемый элемент массива evnt . Если свободных элементов нет, выдается сообщение об ошибке. Поскольку массив evnt рассчитан на хранение лишь трех элементов, он может принять только три обработчика событий. При удалении заданного обработчика событий вызывается remove-средство, и в массиве evnt выполняется поиск ссылки на обработчик, переданной в параметре value . Если ссылка найдена, в соответствующий элемент массива помещается значение null , что равнозначно удалению обработчика из списка. При генерировании события вызывается метод OnSomeEvent() . Он в цикле просматривает массив evnt , по очереди вызывая каждый обработчик событий. Как показано в предыдущих примерах, при необходимости относительно нетрудно реализовать собственный механизм хранения обработчиков событий. Для большинства приложений все же лучше использовать стандартный механизм хранения, в котором не используются событийные средства доступа. Однако в определенных ситуациях форма event -инструкции, ориентированной на событийные средства доступа, может оказаться весьма полезной. Например, если в программе обработчики событий должны выполняться в порядке уменьшения приоритетов, а не в порядке их добавления в событийную цепочку, то для хранения таких обработчиков можно использовать очередь по приоритету. Смешанные средства обработки событий События можно определять в интерфейсах. “Поставкой” событий должны заниматься соответствующие классы. События можно определять как абстрактные. Обеспечить реализацию такого события должен производный класс. Однако события, реализованные 426 Часть I. Язык C# с использованием средств доступа add и remove , абстрактными быть не могут. Любое событие можно определить с помощью ключевого слова sealed . Событие может быть виртуальным, т.е. его можно переопределить в производном классе. Рекомендации по обработке событий в среде .NET Framework C# позволяет программисту создавать события любого типа. Однако в целях компонентной совместимости со средой .NET Framework необходимо следовать рекомендациям, подготовленным Microsoft специально для этих целей. Центральное место в этих рекомендациях занимает требование того, чтобы обработчики событий имели два параметра. Первый должен быть ссылкой на объект, который будет генерировать событие. Второй должен иметь тип EventArgs и содержать остальную информацию, необходимую обработчику. Таким образом, .NET-совместимые обработчики событий должны иметь следующую общую форму записи: void handler(object source , EventArgs arg ) { // ... } Обычно параметр source передается вызывающим кодом. Параметр типа EventArgs содержит дополнительную информацию, которую в случае ненадобности можно проигнорировать. Класс EventArgs не содержит полей, которые используются при передаче дополнительных данных обработчику; он используется в качестве базового класса, из которого можно выводить класс, содержащий необходимые поля. Но поскольку многие обработчики обходятся без дополнительных данных, в класс EventArgs включено статическое поле Empty , которое задает объект, не содержащий никаких данных. Ниже приведен пример, в котором создается .NET-совместимое событие. // А .NET-совместимое событие. using System; // Создаем класс, производный от класса EventArgs. class MyEventArgs : EventArgs { public int eventnum; } // Объявляем делегат для события. delegate void MyEventHandler(object source, MyEventArgs arg); // Объявляем класс события. class MyEvent { static int count = 0; public event MyEventHandler SomeEvent; // Этот метод генерирует SomeEvent-событие. public void OnSomeEvent() { MyEventArgs arg = new MyEventArgs(); if(SomeEvent != null) { Глава 15. Делегаты и события 427 arg.eventnum = count++; SomeEvent(this, arg); } } } class X { public void handler(object source, MyEventArgs arg) { Console.WriteLine("Событие " + arg.eventnum + " получено объектом X."); Console.WriteLine("Источником является класс " + source + "."); Console.WriteLine(); } } class Y { public void handler(object source, MyEventArgs arg) { Console.WriteLine("Событие " + arg.eventnum + " получено объектом Y."); Console.WriteLine("Источником является класс " + source + "."); Console.WriteLine(); } } class EventDemo { public static void Main() { X ob1 = new X(); Y ob2 = new Y(); MyEvent evt = new MyEvent(); // Добавляем обработчик handler() в список событий. evt.SomeEvent += new MyEventHandler(ob1.handler); evt.SomeEvent += new MyEventHandler(ob2.handler); // Генерируем событие. evt.OnSomeEvent(); evt.OnSomeEvent(); } } Вот как выглядят результаты выполнения этой программы: Событие 0 получено объектом X. Источником является класс MyEvent. Событие 0 получено объектом Y. Источником является класс MyEvent. Событие 1 получено объектом X. Источником является класс MyEvent. Событие 1 получено объектом Y. Источником является класс MyEvent. В этом примере класс MyEventArgs выводится из класса EventArgs . В классе MyEventArgs добавлено только одно “собственное” поле — eventnum . В соответствии 428 Часть I. Язык C# с требованиями .NET Framework делегат для обработчика событий MyEventHandler теперь принимает два параметра. Как разъяснялось выше, первый из них представляет собой объектную ссылку на генератор событий, а второй — ссылку на класс EventArgs или производный от класса EventArgs . В данном случае здесь используется ссылка на объект типа MyEventArgs Использование встроенного делегата EventHandler Для многих событий параметр типа EventArgs не используется. Для упрощения процесса создания кода в таких ситуациях среда .NET Framework включает встроенный тип делегата, именуемый EventHandler . Его можно использовать для объявления обработчиков событий, которым не требуется дополнительная информация. Рассмотрим пример использования типа EventHandler // Использование встроенного делегата EventHandler. using System; // Объявляем класс события. class MyEvent { public event EventHandler SomeEvent; // Объявление // использует делегат EventHandler. // Этот метод вызывается для генерирования // SomeEvent-событие. public void OnSomeEvent() { if(SomeEvent != null) SomeEvent(this, EventArgs.Empty); } } class EventDemo { static void handler(object source, EventArgs arg) { Console.WriteLine("Событие произошло."); Console.WriteLine("Источником является класс " + source + "."); } public static void Main() { MyEvent evt = new MyEvent(); // Добавляем обработчик handler() в список событий. evt.SomeEvent += new EventHandler(handler); // Генерируем событие. evt.OnSomeEvent(); } } В данном случае параметр типа EventArgs не используется и вместо него передается объект-заполнитель EventArgs.Empty . Результаты выполнения этой программы весьма лаконичны: Событие произошло. Источником является класс MyEvent. Глава 15. Делегаты и события 429 Учебный проект: использование событий События часто используются в таких средах с ориентацией на передачу сообщений, как Windows. В подобной среде программа просто ожидает до тех пор, пока не получит сообщение, а затем выполняет соответствующие действия. Такая архитектура прекрасно подходит для обработки событий в стиле языка C#, позволяя создавать обработчики событий для различных сообщений и просто вызывать обработчик при получении определенного сообщения. Например, с некоторым событием можно было бы связать сообщение, получаемое в результате щелчка левой кнопкой мыши. Тогда после щелчка левой кнопкой мыши все зарегистрированные обработчики будут уведомлены о приходе этого сообщения. Несмотря на то что разработка Windows-программ, в которых демонстрируется такой подход, выходит за рамки этой главы, все же обрисуем в общих чертах работу этого механизма. В следующей программе создается обработчик событий нажатия клавиш. Событие называется KeyPress , и при каждом нажатии клавиши оно генерируется посредством вызова метода OnKeyPress() // Пример обработки события, связанного с нажатием // клавиши на клавиатуре. using System; // Выводим собственный класс EventArgs, который // будет хранить код клавиши. class KeyEventArgs : EventArgs { public char ch; } // Объявляем делегат для события. delegate void KeyHandler(object source, KeyEventArgs arg); // Объявляем класс события, связанного с нажатием // клавиши на клавиатуре. class KeyEvent { public event KeyHandler KeyPress; // Этот метод вызывается при нажатии // какой-нибудь клавиши. public void OnKeyPress(char key) { KeyEventArgs k = new KeyEventArgs(); if(KeyPress != null) { k.ch = key; KeyPress(this, k); } } } // Класс, который принимает уведомления о нажатии клавиши. class ProcessKey { public void keyhandler(object source, KeyEventArgs arg) { Console.WriteLine( "Получено сообщение о нажатии клавиши: " + arg.ch); } } 430 Часть I. Язык C# // Еще один класс, который принимает уведомления // о нажатии клавиши. class CountKeys { public int count = 0; public void keyhandler(object source, KeyEventArgs arg) { count++; } } // Демонстрируем использование класса KeyEvent. class KeyEventDemo { public static void Main() { KeyEvent kevt = new KeyEvent(); ProcessKey pk = new ProcessKey(); CountKeys ck = new CountKeys(); char ch; kevt.KeyPress += new KeyHandler(pk.keyhandler); kevt.KeyPress += new KeyHandler(ck.keyhandler); Console.WriteLine("Введите несколько символов. " + "Для останова введите точку."); do { ch = (char) Console.Read(); kevt.OnKeyPress(ch); } while(ch != '.'); Console.WriteLine("Было нажато " + ck.count + " клавиш."); } } При выполнении этой программы можно получить такие результаты: Введите несколько символов. Для останова введите точку. тест. Получено сообщение о нажатии клавиши: т Получено сообщение о нажатии клавиши: е Получено сообщение о нажатии клавиши: с Получено сообщение о нажатии клавиши: т Получено сообщение о нажатии клавиши: . Было нажато 5 клавиш. Эта программа начинается с выведения класса KeyEventArgs , который используется для передачи сообщения о нажатии клавиши обработчику событий. Затем делегат KeyHandler определяет обработчик для событий, связанных с нажатием клавиши на клавиатуре. Эти события инкапсулируются в классе KeyEvent Программа для обработки нажатий клавиш создает два класса: ProcessKey и CountKeys . Класс ProcessKey включает обработчик с именем keyhandler() , который отображает сообщение о нажатии клавиши. Класс CountKeys предназначен для хранения текущего количества нажатых клавиш. В методе Main() создается объект класса KeyEvent . Затем создаются объекты классов ProcessKey и CountKeys , a ссылки на их методы keyhandler() добавляются в список вызовов, реализуемый с помощью событийного объекта kevt.KeyPress . Затем начинает работать цикл, в котором при каждом нажатии клавиши вызывается метод kevt.OnKeyPress() , в результате чего зарегистрированные обработчики уведомляются о событии. |