Раніше наголошувалося, що якщо в одному з методів списку делегата генерується виключення, наступні методи не викликаються. Це можна уникнути, якщо забезпечити явний перебір всіх методів в блоці, що перевіряється, і обробляти виникаючі виключення. Всі методи, задані в екземплярі делегата, можна отримати за допомогою успадкованого методу GetInvocationList. Цей прийом ілюструє лістинг 10.6, що є зміненим варіантом лістингу 10.1.
Лістинг 10.6. Перехоплення виключень при виклику делегата
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace examp70
{
delegate void Del ( ref string s );
class Classl
{
public static void Cool ( ref string s )
{
Console.WriteLine( "викликаний метод Cool" );
s = s.Replace('l', '1').Replace('O', '0').Replace('o', '0');
}
public static void Hack ( ref string s )
{
Console.WriteLine( "викликаний метод Hack" );
char[] mas = s.ToCharArray();
s = "";
for (int i = 0; i < mas.Length; ++i)
s+= ((i & 1) == 0)? char.ToUpper(mas[i]):mas[i];
}
public static void BadHack ( ref string s )
{
Console.WriteLine( "викликаний метод Badhack" );
throw new Exception(); // імітація помилки
}
static void Main()
{
string s = "cool hackers";
Del d = new Del(Cool); // створення екземпляра делегата
d += new Del(BadHack); // доповнення списку методів
d += new Del(Hack); // доповнення списку методів
foreach ( Del fun in d.GetInvocationList() )
{
try
{
fun( ref s ); // виклик кожного методу із списку
}
catch (Exception er)
{
Console.WriteLine(er.Message);
Console.WriteLine( "Exception in method " + fun.Method.Name);
}
}
Console.WriteLine( "результат - " + s );
}
}
}
Результат роботи програми:
викликаний метод Cool
викликаний метод BadHack
Exception of type System.Exception was thrown.
Exception in method BadHack
викликаний метод Hack
результат – C001 hAcKeRs
У цій програмі окрім методу базового класу GetInvocationList використана властивість Method. Цю властивість повертає результат типу MethodInfo. Клас MethodInfo містить множину властивостей і методів, що дозволяють отримати повну інформацію про метод, наприклад його специфікатори доступу, ім'я і тип повертаємого значення.
Події
Подія - це елемент класу, що дозволяє йому посилати іншим об'єктам повідомлення про зміну свого стану. При цьому для об'єктів, що є спостерігачами події, активізуються методи-обробники цієї події. Обробники мають бути зареєстровані в об'єкті-джерелі події. Таким чином, механізм подій формалізує на мовному рівні патерн “спостерігач”, який розглядався в попередньому розділі.
Механізм подій можна також описати за допомогою моделі “публікація - підписка”: один клас, що є відправником (sender) повідомлення, публікує події, які він може ініціювати, а інші класи, повідомлення, що є одержувачами (receivers), підписуються на отримання цих подій.
Події побудовані на основі делегатів: за допомогою делегатів викликаються методи-обробники подій. Тому створення події в класі складається з наступних частин:
опис делегата, задаючого сигнатуру обробників подій;
опис події;
опис методу (методів), що ініціюють подію.
Синтаксис події схожий на синтаксис делегата:
[ атрибути ] [ специфікатори ] event тип ім'я_події
Для подій застосовуються специфікатори new, public, protected, internal, private, static, virtual, sealed, override, abstract і extern, які вивчалися при розгляді методів класів. Подія може бути статичною (static) або звичайною. При цьому вона відповідно пов'язана з класом або з екземпляром класу.
Тип події - це тип делегата, на якому заснована подія.
Приклад опису делегата і відповідної йому події:
public delegate void Del( object о ); // оголошення делегата
class А
{
public event Del Oops; // оголошення події
…
}
Обробка подій виконується в класах-одержувачах повідомлення. Для цього в них описуються методи-обробники подій, сигнатура яких відповідає типу делегата. Кожен об'єкт (не клас!), охочий отримувати повідомлення, повинен зареєструвати в об'єкті-відправнику цей метод.
Як бачите, це в точності той же самий механізм, який розглядався в попередньому розділі. Єдина відмінність полягає в тому, що при використанні подій не потрібно описувати метод, реєструючий обробники, оскільки події підтримують операції += і -=, що додають обробник в список і що видаляють його із списку.
Подія - це зручна абстракція для програміста. Насправді вона складається із закритого статичного класу, в якому створюється екземпляр делегата, і двох методів, призначених для додавання і видалення обробника із списку цього делегата.
У лістингу 10.7 приведений код з лістингу 10.2, перероблений з використанням подій.
Лістинг 10.7. Сповіщення спостерігачів за допомогою подій
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace examp71
{
public delegate void Del(); // оголошення делегата
class Subj // клас-джерело
{
public event Del Oops; // оголошення події
public void CryOops() // метод, що ініціює подію
{
Console.WriteLine("OOPS!");
if (Oops != null) Oops();
}
}
class ObsA // клас-спостерігач
{
public void Do() // реакція на подію джерела
{
Console.WriteLine("Бачу, що OOPS!");
}
}
class ObsB // клас-спостерігач
{
public static void See() // реакція на подію джерела
{
Console.WriteLine("Я теж бачу, що OOPS!");
}
}
class Classl
{
static void Main()
{
Subj s = new Subj(); // об'єкт класу-джерела
ObsA ol = new ObsA(); // об'єкти
ObsA o2 = new ObsA(); // класу-спостерігача
s.Oops += new Del( ol.Do ); // додавання
s.Oops += new Del( o2.Do ); // обробників
s.Oops += new Del ( ObsB.See ); // до події
s.CryOops(); // ініціація події
}
}
}
Зовнішній код може працювати з подіями тільки таким чином: додавати обробники в список або видаляти їх, оскільки поза класом можуть використовуватися тільки операції += і -=. Тип результату цих операцій - void, на відміну від операцій складного привласнення для арифметичних типів. Іншого способу доступу до списку обробників немає.
Усередині класу, в якому описана подія, з ним можна звертатися, як із звичайним полем, що має тип делегата: використовувати операції відношення, привласнення і так далі. Значення події за умовчанням - null. Наприклад, в методі Cryoops виконується перевірка на null для того, щоб уникнути генерації виключення System.NullReferenceException.
У бібліотеці .NET описана величезна кількість стандартних делегатів, призначених для реалізації механізму обробки подій. Більшість цих класів оформлена по одних і тих же правилах:
– ім'я делегата закінчується суфіксом EventHandler;
– делегат отримує два параметри:
– перший параметр задає джерело події і має тип object;
– другий параметр задає аргументи події і має тип EventArgs або похідний від нього.
Якщо обробникам події потрібна специфічна інформація про подію, то для цього створюють клас, похідний від стандартного класу EventArgs, і додають в нього необхідну інформацію. Якщо делегат не використовує таку інформацію, можна не описувати делегат і власний тип аргументів, а обійтися стандартним класом делегата System.EventHandler.
Ім'я обробника події прийнято складати з префікса On і імені події. У лістингу 10.8 приведений приклад з лістингу 10.7, оформлений відповідно до стандартних угод .NET. Знайдіть вісім відмінностей!
Лістинг 10.8. Використання стандартного делегата EventHandler
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace examp72
{
class Subj
{
public event EventHandler Oops;
public void CryOops()
{
Console.WriteLine("OOPS!");
if ( Oops != null ) Oops (this, null );
}
}
class ObsA
{
public void OnOops(object sender, EventArgs e )
{
Console.WriteLine("Бачу, що OOPS!");
}
}
class ObsB
{
public static void OnOops( object sender, EventArgs e )
{
Console.WriteLine("Я теж бачу, що OOPS!");
}
}
class Classl
{
static void Main()
{
Subj s = new Subj();
ObsA ol = new ObsA();
ObsA o2 = new ObsA();
s.Oops += new EventHandler(ol.OnOops);
s.Oops += new EventHandler(o2.OnOops);
s.Oops += new EventHandler(ObsB.OnOops);
s.CryOops ();
}
}
}
Можна спростити цю програму, використовуючи можливість неявного створення делегатів при реєстрації обробників подій. Відповідний варіант приведений в лістингу 10.9.
Лістинг 10.9. Використання делегатів і анонімних методів
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace examp73
{
class Subj
{
public event EventHandler Oops;
public void CryOops()
{
Console.WriteLine("OOPS!" );
if ( Oops != null ) Oops( this, null );
}
}
class ObsA
{
public void OnOops(object sender, EventArgs e )
{
Console.WriteLine("Бачу, що OOPS!" );
}
}
class ObsB
{
public static void OnOops(object sender, EventArgs e )
{
Console.WriteLine("Я теж бачу, що OOPS!" );
}
}
class Classl
{
static void Main()
{
Subj s = new Subj();
ObsA o1 = new ObsA();
ObsA o2 = new ObsA();
s.Oops += o1.OnOops;
s.Oops += o2.OnOops;
s.Oops += ObsB.OnOops;
s.Oops += delegate(object sender, EventArgs e)
{
Console.WriteLine("Я з вами!");
};
s.CryOops ();
}
}
}
Події включені в багато стандартних класів .NET, наприклад, в класи простору імен Windows.Forms, використовувані для розробки Windows-додатків.