На главную

Библиотека Интернет Индустрии I2R.ru

Rambler's Top100

Малобюджетные сайты...

Продвижение веб-сайта...

Контент и авторское право...

Забобрить эту страницу! Забобрить! Блог Библиотека Сайтостроительства на toodoo
  Поиск:   
Рассылки для занятых...»
I2R » И2Р Программы » Программирование » C, C++

Использование делегатов и событий

Неотъемлемой частью в практике программирования стало использование обратных вызовов (callback) и уведомлений (notifications). Основанные на них приемы нашли широкое применение в написании кода. К тому же в последнее время стало все больше распространяется программирование с использованием событий (заметим, что события реализуются с помощью уведомлений). Для реализации обратных вызовов и уведомлений в языках C и C++ используют указатели на функции. Такой подход несет в себе некоторые недостатки, так как не предоставляет типовой защищенности, что может привести к появлению ошибок связанных с преобразованием типов, которые не выявляются на стадии компиляции. Поскольку типовая защищенность - одно из достоинств C#, в этом языке Вы не можете объявить указатель на функцию. Для реализации вышеупомянутых действий (обратных вызовов и уведомлений) в C# используются делегаты. Делегат представляет собой подобие указателя на функцию. Он сохраняет его (указателя на функцию) преимущества, при этом избавляя от его недостатков. Использование делегатов позволяет добиться тех же результатов, что и при использования указателей на функции в C/C++, не теряя типовой защищенности. Данная статься рассматривает использование делегатов и событий в программах на C#. Особое внимание уделяется их внутренней структуре, а также реализации событийной модели в CLR (.NET Runtime).

Объявление делегатов


Поскольку CLR объектно-ориентированная среда, в ней все так или иначе связано с классами и объектами. В полной мере это относится и к делегатам. Делегаты в CLR реализованы в виде классов, производных от базового класса делегата: System.Delegate или System.MulticastDelegate. Строго говоря, делегат - это класс, содержащий данные о сигнатуре метода. Экземпляр делегата (delegae instance) - объект, который позволяет привязаться к конкретному методу, соответствующему определенной сигнатуре (подобно объявлению указателя на функцию в C/C++). Как и указатель на функцию делегат может быть передан в виде аргумента.

Рассмотрим действия, которые надо проделать, чтобы воспользоваться делегатами. Для начала необходимо объявить делегат. Для объявления делегатов в C# используется ключевое слово delegate. То есть концепция делегатов непосредственно поддерживается синтаксисом языка. Ниже приведен пример объявления делегата:

delegate string MyDelegate(int x);

Все что мы получили от вышеприведенного объявления - это новый тип (класс). Это и следовало было ожидать, так как класс и является пользовательским типом данных. Что определяет этот класс? Данный класс определяет форму метода - его сигнатуру. В сигнатуру метода входит тип возвращаемого значения и список аргументов. В нашем случае, метод должен возвращать строку и принимать один целочисленный аргумент. Так как делегат является классом, он может быть объявлен как в пространстве имен, так и в классе. Оба ниже приведенных листинга содержат корректные объявления:

//листинг 1
using System;

delegate string MyDelegate(int x);

class TestApp {
	public static void Main() {
	:		
	}//public static void Main()
}//class TestApp

//листинг 2
using System;

class TestApp {
	delegate string MyDelegate(int x);

	public static void Main() {
	:		
	}//public static void Main()
}//class TestApp

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

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

MyDelegate dg;		//объявление экземпляра делегата

В этом фрагменте кода MyDelegate - имя класса, а dg - имя объекта. Как и любой объект в C# перед использованием экземпляр делегата необходимо инициализировать. И в этом случае экземпляр делегата инициализируется как и экземпляр любого класса, при помощи оператора new.

MyDelegate dg;			//объявление экземпляра делегата
dg = new MyDelegate(MethodName);	//инициализация экземпляра делегата

Однако здесь есть одно очень существенное отличие. Как видно из вышеприведенного фрагмента кода, конструктору делегата передается идентификатор метода (имя), на который будет указывать экземпляр делегата. Отличие заключается в том, что при попытке передать идентификатор метода конструктору обычного класса, независимо от типа аргумента конструктора, будет выдано следующее сообщение об ошибке:

de.cs(31,37): error CS0654: Method 'TestApp.MyMethod(int)' 
			referenced without an argument list

Данный факт объясняется тем, что концепция делегатов поддерживается синтаксисом C#. Передача имени метода допустима только конструкторам делегатов. Как и для обычных классов, при объявлении экземпляр делегата можно инициализировать:

// объявление и инициализация экземпляра делегата
MyDelegate dg = new MyDelegate(MethodName);

Экземпляр делегата может быть привязан, как к статическому так и к нестатическому методу класса. В случае со статическим методом, перед именем метода указывается имя класса. Если имя класса опущено, подразумевается класс, в реализации которого встречается инициализация экземпляра делегата. В случае с нестатическим методом, перед его именем указывается имя объекта Если имя объекта опущено, предполагается, что используется текущий экземпляр (this):

dg = new MyDelegate(ClassName.MethodName);	//статический метод

dg = new MyDelegate(ObjectName.MethodName);	//нестатический метод

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

dg(55);

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

sting str = dg(55);

А теперь рассмотрим простой пример использования делегата:

//листинг 3
using System;

//объявляем делегат
delegate string MyDelegate(int arg);

class TestApp {
	
	//статический метод 
	public static string MyMethod(int arg) {
		return arg.ToString();
	}//public static string MyMethod(int arg)

	public static void Main() {
		//создаем экземпляр делегата и инициализируем его
		MyDelegate dg = new MyDelegate(TestApp.MyMethod);

		//вызываем метод, на который указывает делегат 
		string returnValue = dg(25);

		//выводим полученное значение
		Console.WriteLine(returnValue);

	}//public static void Main()
}//class TestApp

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

Типы делегатов


Существует два типа делегатов: одиночные (singlecast delegate) и комбинированные (multicast delegate). Пример использования одиночного делегата был рассмотрен выше. С помощью одиночного делегата за один раз может быть вызван единственный метод, в то время как с помощью комбинированного делегата можно вызвать либо один, либо несколько методов одновременно. Одиночный делегат мы умеем определять, а как определить комбинированный делегат? Различие заключается в сигнатуре делегата, а точнее - в типе возвращаемого значения. Если делегат объявлен с типом возвращаемого значения void, он является комбинированным. Это вполне логично, поскольку комбинированные делегаты одновременно могут вызвать более одного метода, в то время как сам вызов может возвращать не более одного значения. Комбинированные делегаты наследуют реализацию от класса System.MulticastDelegate. В случае объявления с типом отличным от void делегат является одиночным. Выше был определен делегат с типом возвращаемого значения string. Одиночные делегаты являются производными от System.Delegate. Для подтверждения можно воспользоваться утилитой IL Disassembler, входящую в состав .NET SDK. Ниже показано окно IL Disassembler изображающее откомпилированный пример из первого пункта. В этом примере создается одиночный делегат MyDelegate, как видно он наследует реализацию от System.Delegate.

Изменим пример следующим образом:

//листинг 4
using System;

//объявляем делегат
delegate void MyDelegate(int arg);	
		// тип возвращаемого значения меняется со string на void

class TestApp {
	
	//статический метод 
	public static void MyMethod(int arg) {		
		Console.WriteLine(arg);
	}//public static void MyMethod(int arg)

	public static void Main() {
		//создаем экземпляр делегата и инициализируем его
		MyDelegate dg = new MyDelegate(TestApp.MyMethod);

		//вызываем метод, на который указывает делегат 
dg(25);
	}//public static void Main()
}//class TestApp

Мы изменили объявление делегата с:

delegate string MyDelegate(int arg);

на

delegate void MyDelegate(int arg);

Откомпилируем и откроем исполняемый файл с помощью IL Disassembler:

Как вы видите, делегат с типом возвращаемого значения void расширяет класс System.MulticastDelegate, то есть является комбинированным делегатом.

Внутренняя структура делегатов


Мы рассмотрели простейший пример использования делегатов. Теперь постараемся разобраться в их внутренней структуре. Сначала рассмотрим одиночные делегаты, потому как комбинированные делегаты являются их расширением. А немного позже мы разберемся с использованием комбинированных делегатов. Все что касается одиночных делегатов применимо и к комбинированным. Убедится в этом можно посмотрев на иерархию наследования классов System.Delegate и System.MulticastDelegate.

Также приведем объявление обоих классов:

public abstract class Delegate : ICloneable, Iserializable

и

public abstract class MulticastDelegate : Delegate

Оба класса являются абстрактными. Их реализация находится в сборке mscorlib.dll пространства имен System.

Одиночные делегаты


Как Вы помните, одиночные делегаты являются классами, производными от System.Delegate. Чтобы ознакомится с этим классом откройте документацию на него. Среди методов и свойств данного класса сейчас нас особо интересуют два свойства: Target и Method. Вот их объявления:

public object Target 
public MethodInfo Method 

Если делегат указывает на нестатический метод, то при его инициализации свойству Target присваивается ссылка на экземпляр конкретного объекта - контекст объекта. В случае же со статическим методом его значение null. Второе свойство - Method содержит данные о методе. В отличии от первого свойства его значение никогда не равно null. Оба свойства являются свойствами только для чтения: Вы не можете непосредственно модифицировать их значение. Значение данных свойств устанавливается при инициализации делегата.

Продолжая изучение класса System.Delegate, рассмотрим его конструктор. Класс System.Delegate имеет два конструктора, приведем их объявление:

protected Delegate(
   Type target,
   string method
);

Данный конструктор предназначен для инициализации делегатов, указывающих на статический метод. Первый аргумент определяет тип (класс), в котором находится статический метод. Второй аргумент задает строку с именем самого метода.

protected Delegate(
   object target,
   string method
);

А этот конструктор предназначен для инициализации делегатов, указывающих на нестатический метод. Первый аргумент определяет контекст объекта. Смысл второго аргумента аналогичен смыслу второго аргумента в первом конструкторе. Однако ни один из этих конструкторов Вам не понадобится при программировании на C#, поскольку компилятор сам генерирует производный класс делегата и вызывает конструктор, скрывая от Вас детали реализации, тем самым, предоставляя более простой синтаксис объявления делегатов.

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

string returnValue = dg(25);

компилятор распознает, что dg это экземпляр делегата. Вызов происходит посредством рефлексии. Рефлексия - это замечательный механизм, позволяющий динамически работать с типами, читать информацию о типе и вызывать любые методы класса во время выполнения. Как Вы помните, каждый делегат имеет два свойства: Method и Target. Свойство Method имеет тип MethodInfo. В классе MethodInfo существует метод Invoke (вызвать), позволяющий динамически вызвать метод, данные о котором хранятся в объекте. Компилятор использует значение свойств Target и Method делегата и генерирует код для вызова Invoke. Если Вы не знакомы с рефлексией, обязательно с ней познакомьтесь, в будущем Вы еще неоднократно с ней встретитесь. Только что Вы увидели наглядный пример ее применения. Концепция атрибутов также основана на использовании рефлексии.

Комбинированные делегаты


Рассмотрим следующий пример кода:

//листинг 5
using System;

//объявляем делегат
delegate void MyDelegate(int arg);		// комбинированный делегат

class TestApp {
	
	//статический метод 
	public static void MyMethod(int arg) {		
		Console.WriteLine(arg);
	}//public static void MyMethod(int arg)

	public static void Main() {
		//создаем экземпляр делегата и инициализируем его
		MyDelegate dg = new MyDelegate(TestApp.MyMethod);

		//вызываем метод, на который указывает делегат
dg(25);
	}//public static void Main()
}//class TestApp

Это простейший пример использования комбинированного делегата. Однако в этом случае он идентичен применению одиночного делегата, потому как вызывается единственный метод. А как Вы помните, комбинированные делегаты способны ссылаться на более чем один метод. Чтобы создать комбинированный делегат, который указывает на более чем один метод, необходимо комбинировать делегаты. Для этого в классе System.Delegate существует статический метод Combine:

public static Delegate Combine(Delegate[])
public static Delegate Combine(Delegate, Delegate)

Рассмотрим пример использования этого метода:

//листинг 6
using System;

delegate void MyMulticastDelegate(int arg);

class TestApp {
	//метод 1
	public static void Method1(int arg) {
		Console.WriteLine("Method1: ", arg);
	}//public static void Method1(int arg)

	//метод 2
	public static void Method2(int arg) {
		Console.WriteLine("Method2: ", arg);
	}//public static void Method2(int arg)


    public static void Main() {
	//объявление и инициализация делегатов для метода 1 и метода 2
        MyMulticastDelegate mcdg1 = new MyMulticastDelegate(TestApp.Method1);
        MyMulticastDelegate mcdg2 = new MyMulticastDelegate(TestApp.Method2);
 
	//комбинированный делегат (будет вызывать оба метода)
        MyMulticastDelegate mcdg3;

	//комбинируем делегаты
        mcdg3 = (MyMulticastDelegate)Delegate.Combine(mcdg1, mcdg2);
	mcdg3(100);

   }//public static void Main()
}//class TestApp

В приведенном примере сначала создаются два делегата: mcdg1 и mcdg2. Затем с помощью метода Combine они комбинируются. В результате мы получаем комбинированный делегат mcdg3, результатом вызова которого будет вызов обоих методов: делегата mcdg1 и mcdg2. Чтобы за один вызов Combine скомбинировать более двух делегатов можно воспользоваться перегруженной версией этого метода: Вот пример его использования:

//листинг 7
using System;

delegate void MyMulticastDelegate(int arg);

class TestApp {
	public static void Method1(int arg) {
		Console.WriteLine("Method1: ", arg);
	}//public static void Method1(int arg)

	public static void Method2(int arg) {
		Console.WriteLine("Method2: ", arg);
	}//public static void Method2(int arg)

	public static void Method3(int arg) {
		Console.WriteLine("Method3: ", arg);
	}//public static void Method3(int arg)


    public static void Main() {
        
        MyMulticastDelegate mcdg1 = new MyMulticastDelegate(TestApp.Method1);
        MyMulticastDelegate mcdg2 = new MyMulticastDelegate(TestApp.Method2);
        MyMulticastDelegate mcdg3 = new MyMulticastDelegate(TestApp.Method3);
 
        MyMulticastDelegate mcd4;
	
	//объявляем и инициализируем массив делегатов
	Delegate[] dgArr = {mcdg1, mcdg2, mcdg3};

        mcd4 = (MyMulticastDelegate)Delegate.Combine(dgArr);

	mcd4(100); 

   }//public static void Main()
}//class TestApp

Как Вы наверное заметили, возвращаемое значение метода Combine необходимо явно преобразовать к соответствующему типу делегата, так как метод возвращает значение с типом Delegate:

mcdg4 = (MyMulticastDelegate) Delegate.Combine(...);

Структура комбинированного делегата


С точки зрения реализации, комбинированный делегат лишь немногим отличается от одиночного. Различие заключается в следующем: класс System.MulticastDelegate содержит дополнительное скрытое поле _prev, содержащее ссылку на другой комбинированный делегат:

А сама сущность - комбинированный делегат представляется в виде связанного списка делегатов с типом возвращаемого значением void. Действительно, одиночный и комбинированный делегаты имеют очень много общего, и Вы вправе использовать комбинированные делегаты (с типом void) как одиночные (листинг 5). Дополнительное преимущества комбинированных делегатов в том, что Вы можете объединить их в связанный список, используя метод Combine, и одновременно вызывать более одного метода. Еще раз посмотрим на метод Combine:

mcdg3 = (MyMulticastDelegate)Delegate.Combine(mcdg1, mcdg2);

Все что делает данный метод, это присваивает полю _prev делегата mcdg2 ссылку на делегат mcdg1, и возвращает ссылку на mcdg2. В этом случае mcdg2 будет являться головным элементом списка. Если же mcdg1 является связанным списком, то mcdg2 добавится в этот список. Рассмотрим еще пару полезных методов. В дополнение к методу Combine в классе System.Delegate также содержится метод Remove:

public static Delegate Remove(
   Delegate source,
   Delegate value
);

Он удаляет делегат, определенный в аргументе value из связанного списка делегатов, определенного аргументом source. Второй метод GetInvocationList:

public virtual Delegate[] GetInvocationList();

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

Обратные вызовы


Здесь мы рассмотрим использование делегатов для реализации обратных вызовов. Рассмотрим простой пример:

//листинг 8
using System;

delegate int CallBackMethod(int arg1, int arg2);

class TestApp {
	public static int MyCallBackMethod(int arg1, int arg2) {
		//некоторый код
		Console.WriteLine("-",arg1,arg2);
		return 0;
	}//public static void MyCallBackMethod

	//метод в качестве аргумента берет делегат
	public static void SimpleMethod(CallBackMethod cbm) {
		//вызов callback метода
		cbm(10,15);
	}//public static void SimpleMethod(CallBackMethod cbm)

	public static void Main() {
		Console.WriteLine("*** CallBack method ***");
		CallBackMethod callBackMethod = new CallBackMethod(MyCallBackMethod);
		SimpleMethod(callBackMethod);

	}//public static void Main()
}//class TestApp

Для начала нам необходимо определиться с сигнатурой callback-метода и объявить, соответствующий ей делегат (в нашем примере: CallBackMethod). Затем реализуем callback-метод: MyCallBackMethod, на который будет ссылаться наш делегат. Метод SimpleMethod осуществляет обратный вызов посредством делегата cbm, который передается в виде аргумента. Особенность callback-метода в том, что Вы передаете его в виде аргумента другому методу, а не вызываете его непосредственно.

Событийная модель


Событийная модель в CLR строиться на основе комбинированных делегатов. В событийной модели используется схема издатель/подписчик. Класс, предоставляющий события другим классом, способен уведомлять их о переходе в определенное состояние. В свою очередь желающие получить уведомление обязательно должны зарегистрироваться. Регистрация заключается в передаче классу-издателю делегата, указывающего на метод-обработчик события. Метод-обработчик находится в классе-подписчике и включает код, который должен быть выполнен в ответ на событие, произошедшее в классе-издателе.. Поскольку с делегатами мы разобрались, давайте попробуем предоставить собственную реализацию механизма, обеспечивающего работу событий.

//листинг 9
using System;

//класс делегата
delegate void SimpleMessage(string msg);

//класс, предоставляющий события
class ClassWithEvents {
    //экземпляр комбинированного делегата
    SimpleMessage subscribers;

    //метод для регистрации подписчика
    public void AddSubscriber(SimpleMessage sbcr) {
  	   if (subscribers==null) subscribers = sbcr;
  	   else subscribers = (SimpleMessage)Delegate.Combine(subscribers, sbcr);
    }//public void AddSubscriber(SimpleMessage sbcr)
	
    //метод для удаления подписчика
    public void RemoveSubscriber(SimpleMessage sbcr) {
	   if (subscribers!=null) 
  		subscribers = (SimpleMessage)Delegate.Remove(subscribers,sbcr);
    }//public void RemoveSubscriber(SimpleMessage sbcr)
	
    //метод для генерация события
    public void RaiseEvent() {
	   //если делегат ссылается хотя бы на один метод, то вызываем его
 	   if (subscribers!=null) subscribers("Message for subscriber");
    }//public void RaiseEvent()
		
}//class ClassWithEvents

class TestApp {
	//обработчик события 1
	public static void Method1(string msg) {
		Console.WriteLine("Method1: ",msg);
	}//public static void Method1(string msg)

	//обработчик события 2
	public static void Method2(string msg) {
		Console.WriteLine("Method2: ",msg);
	}//public static void Method2(string msg)

	//обработчик события 3
	public static void Method3(string msg) {
		Console.WriteLine("Method3: ",msg);
	}//public static void Method3(string msg)


	public static void Main() {
		Console.WriteLine("*** Delegates & Events ***");
		
		//создаем делегаты
		SimpleMessage dg1 = new SimpleMessage(Method1);
		SimpleMessage dg2 = new SimpleMessage(Method2);
		SimpleMessage dg3 = new SimpleMessage(Method3);

		
		//создаем экземпляр класса-издателя
		ClassWithEvents objectWithEvent = new ClassWithEvents();
		
		//регистрация подписчиков
		objectWithEvent.AddSubscriber(dg1);
		objectWithEvent.AddSubscriber(dg2);
		objectWithEvent.AddSubscriber(dg3);
		
		//генерируем событие
		objectWithEvent.RaiseEvent();
		
		//удаляем подписчиков
		objectWithEvent.RemoveSubscriber(dg3);
		Console.WriteLine("RemoveSubscriber 3");
		objectWithEvent.RaiseEvent();

		objectWithEvent.RemoveSubscriber(dg2);
		Console.WriteLine("RemoveSubscriber 2");
		objectWithEvent.RaiseEvent();

		objectWithEvent.RemoveSubscriber(dg1);
		Console.WriteLine("RemoveSubscriber 1");
		objectWithEvent.RaiseEvent();

	}//public static void Main()
}//class TestApp

Класс ClassWithEvents предоставляет возможность уведомлять подписчиков, посылая им некоторое сообщение. Каждое событие имеет тип, определяющий сигнатуру метода-обработчика события. Этот тип задается делегатом. В классе содержится всего три метода и одно поле. Для подписки на события класс предоставляет метод AddSubscriber, соответственно для отписки RemoveSubscriber. Метод AddSubscriber, заносит переданный делегат в связанный список. Поле subscribers - комбинированный делегат, содержит ссылку на головной элемент связанного списка делегатов подписчиков. Для генерации события используется метод RaiseEvent. В статическом методе Main класса TestApp приведен код, использующий класс-издатель. Он достаточно прост, потому не нуждается в комментариях. Не пожалейте времени и хорошо разберитесь в данном примере. Он дает четкое представление о механизме, обеспечивающем работу событий.

Однако в повседневном программировании было бы довольно утомительно для каждого события предоставлять реализацию методов AddSubscriber и RemoveSubscriber. Поэтому C# поддерживает более простой синтаксис объявления событий. Специально для этого в С# введено ключевое слово event. Используя это ключевое слово, событие из предыдущего примера можно объявить следующим образом:

public event SimpleMessage subscribers;

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

//листинг 10
using System;

delegate void SimpleMessage(string msg);

class ClassWithEvents {
	//объявляем событие
	public event SimpleMessage subscribers;

	public void RaiseEvent() {
		если с событием сопоставлен хотя бы один метод, то вызываем его
		if (subscribers!=null) subscribers("Message for subscriber");
	}//public void RaiseEvent()
		
}//class ClassWithEvents

class TestApp {
	public static void Method1(string msg) {
		Console.WriteLine("Method1: ",msg);
	}//public static void Method1(string msg)

	public static void Method2(string msg) {
		Console.WriteLine("Method2: ",msg);
	}//public static void Method2(string msg)

	public static void Method3(string msg) {
		Console.WriteLine("Method3: ",msg);
	}//public static void Method3(string msg)


	public static void Main() {
		Console.WriteLine("*** Delegates & Events ***");
		
		//создаем делегаты
		SimpleMessage dg1 = new SimpleMessage(Method1);
		SimpleMessage dg2 = new SimpleMessage(Method2);
		SimpleMessage dg3 = new SimpleMessage(Method3);

		ClassWithEvents objectWithEvents = new ClassWithEvents();
		
		//регистрируем подписчиков
		//обратите внимание на синтаксис регистрации
		objectWithEvents.subscribers += dg1;
		objectWithEvents.subscribers += dg2;
		objectWithEvents.subscribers += dg3;
		
		//генерируем событие
		objectWithEvents.RaiseEvent();
		
		//удаление подписчиков
		//здесь также обратите внимание на синтаксис удаления
		objectWithEvents.subscribers -= dg3;
		Console.WriteLine("RemoveSubscriber 3");
		objectWithEvents.RaiseEvent();

		objectWithEvents.subscribers -= dg2;
		Console.WriteLine("RemoveSubscriber 2");
		objectWithEvents.RaiseEvent();

		objectWithEvents.subscribers -= dg1;
		Console.WriteLine("RemoveSubscriber 1");
		objectWithEvents.RaiseEvent();

	}//public static void Main()
}//class TestApp

Класс ClassWithEvents стал проще: в нем нет методов AddSubscriber и RemoveSubscriber. Более того, также упростился синтаксис подписки и отписки на события. Вместо вызова методов можно использовать перегруженные операторы += и -= для подписки и отписки соответственно. Однако ответственность за генерацию события по-прежнему лежит на Вас: метод RaiseEvent не изменился. Преимущества данного подхода очевидны.

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

Что мы видим? Компилятор объявил за нас скрытое поле-делегат: subscribers, который содержит список подписчиков (на рисунке поле выделено курсором). Также компилятор предоставил собственную реализацию методов add_subscribers и remove_subscribers, причем сигнатуры обоих методов идентичны нашей реализации. А теперь посмотрим на их код.

Примечание
Для удобства чтения установите размер шрифта кода в 10. Для этого выберите команду меню View -> Set Fonts -> Disassembly и в диалоговом окне Font установите поле Size равным 10.

Дважды щелкните на имени метода add_subscribers, откроется окно, содержащее код реализации метода на IL (Intermediate Language):


.method public hidebysig specialname instance void 
        add_subscribers(class SimpleMessage 'handler') il managed synchronized
{
  // Code size       24 (0x18)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldarg.0
  IL_0002:  ldfld      class SimpleMessage ClassWithEvents::subscribers
  IL_0007:  ldarg.1
  IL_0008:  call       class [mscorlib]System.Delegate
[mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,
class [mscorlib]System.Delegate)
  IL_000d:  castclass  SimpleMessage
  IL_0012:  stfld      class SimpleMessage ClassWithEvents::subscribers
  IL_0017:  ret
} // end of method ClassWithEvents::add_subscribers

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

public void add_subscribers(SimpleMessage sbcr) {
	subscribers = (SimpleMessage)Delegate.Combine(subscribers, sbcr);
}//public void add_subscribers(SimpleMessage sbcr)

Как Вы видите, он отличается от нашей реализации. Оказывается, что метод add_subscribers просто вызывает, метод Combine. Проверка на null оказалась лишней, по видимому метод Combine об этом позаботился. Поэтому, если все же придется прибегнуть к использованию собственной реализации данных методов, используйте именно такое решение. Теперь попробуем восстановить исходный код remove_subscribers:

.method public hidebysig specialname instance void 
        remove_subscribers(class SimpleMessage 'handler') il managed synchronized
{
  // Code size       24 (0x18)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldarg.0
  IL_0002:  ldfld      class SimpleMessage ClassWithEvents::subscribers
  IL_0007:  ldarg.1
  IL_0008:  call       class [mscorlib]System.Delegate
[mscorlib]System.Delegate::Remove(class [mscorlib]System.Delegate,
class [mscorlib]System.Delegate)
  IL_000d:  castclass  SimpleMessage
  IL_0012:  stfld      class SimpleMessage ClassWithEvents::subscribers
  IL_0017:  ret
} // end of method ClassWithEvents::remove_subscribers

Здесь все аналогично:

public void remove_subscribers (SimpleMessage sbcr) {
	subscribers = (SimpleMessage)Delegate.Remove(subscribers,sbcr);
}//public void remove_subscribers (SimpleMessage sbcr)

Не забывайте использовать именно это решение. А теперь усовершенствуем нашу реализацию:

//листинг 11
 using System;

delegate void SimpleMessage(string msg);

class ClassWithEvents {
	SimpleMessage subscribers;

	public void AddSubscriber(SimpleMessage sbcr) {
		//просто вызываем метод Combine
		subscribers = (SimpleMessage)Delegate.Combine(subscribers, sbcr);
	}//public void AddSubscriber(SimpleMessage sbcr)

	public void RemoveSubscriber(SimpleMessage sbcr) {
		//просто вызываем метод Remove
		subscribers = (SimpleMessage)Delegate.Remove(subscribers,sbcr);
	}//public void RemoveSubscriber(SimpleMessage sbcr)

	public void RaiseEvent() {
		if (subscribers!=null) subscribers("Message for subscriber");
	}//public void RaiseEvent()
		
}//class ClassWithEvents

class TestApp {

	public static void Method1(string msg) {
		Console.WriteLine("Method1: ",msg);
	}//public static void Method1(string msg)

	public static void Method2(string msg) {
		Console.WriteLine("Method2: ",msg);
	}//public static void Method2(string msg)

	public static void Method3(string msg) {
		Console.WriteLine("Method3: ",msg);
	}//public static void Method3(string msg)


	public static void Main() {
		Console.WriteLine("*** Delegates & Events ***");
		
		SimpleMessage dg1 = new SimpleMessage(Method1);
		SimpleMessage dg2 = new SimpleMessage(Method2);
		SimpleMessage dg3 = new SimpleMessage(Method3);

	
		ClassWithEvents objectWithEvent = new ClassWithEvents();
		
		objectWithEvent.AddSubscriber(dg1);
		objectWithEvent.AddSubscriber(dg2);
		objectWithEvent.AddSubscriber(dg3);

		objectWithEvent.RaiseEvent();
		
		objectWithEvent.RemoveSubscriber(dg3);
		Console.WriteLine("RemoveSubscriber 3");
		objectWithEvent.RaiseEvent();

		objectWithEvent.RemoveSubscriber(dg2);
		Console.WriteLine("RemoveSubscriber 2");
		objectWithEvent.RaiseEvent();

		objectWithEvent.RemoveSubscriber(dg1);
		Console.WriteLine("RemoveSubscriber 1");
		objectWithEvent.RaiseEvent();

	}//public static void Main()
}//class TestApp

Да, все же иногда бывает полезно воспользоваться IL Disassembler! На этом с внутренней структурой делегатов и событий все. Далее мы кратко разберем использование событий на практике (при построении пользовательского интерфейса).

Использование событий


Для начала рассмотрим простейший пример использования событий, при программирования графического интерфейса:

//листинг 12
using System;
using System.WinForms;

class MyForm : Form {
	Button button1 = new Button();

	public MyForm() {
		//устанавливаем свойства кнопки
		button1.Text = "Button1";

		//определяем обработчик события
		button1.Click += new EventHandler(button1_Click);

		//добавляем элемент управления в коллекцию формы
		Controls.Add(button1);
	}//public MyForm()

	//обработчик события
	private void button1_Click(object sender, EventArgs e) {
		MessageBox.Show("button1_Click");
	}// private void button1_Click(object sender, EventArgs e)

}//class MyForm : Form

class TestApp {
	public static void Main() {
		Application.Run(new MyForm());
	}//public static void Main()
}//class TestApp

Класс Button содержит событие Click (наследуемое от класса Control), имеющее тип EventHandler. Вот его объявление:

public event EventHandler Click;

А вот он его тип:

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

реализации которого содержится в сборке mscorlib.dll пространства имен System. Как Вы видите, делегат является комбинированным, потому как все делегаты, определяющие тип события должны иметь тип void. Первый аргумент содержит ссылку на источник события - объект, генерирующий событие (в нашем случае это форма - MyForm). Второй аргумент имеет тип EventArgs. Он содержит аргументы, переданные от издателя подписчику. По правде говоря, в нашем случае он не содержит, какой либо полезной информации, поскольку обычно этот класс используется как базовый. Вы вправе определить свой класс, наследующий от EventArgs и внести собственные поля. В классе формы мы определили метод - обработчик события, соответствующий сигнатуре делегата EventHandler, с именем button1_Click. Затем используя перегруженный оператор += регистрируем обработчик события Click.

Рассмотрим еще один пример использования событий - событий таймера. Для этого воспользуемся классом, находящемся в пространстве имен System.WinForms:

public class Timer : Component

Класс содержит событие Tick:

public event EventHandler Tick;

которое возникает через определенный период времени, заданный в свойстве Interval (время задается в миллисекундах). Для включения и выключения таймера вызываются методы Start() и Stop() соответственно. После инициализации таймер находится в выключенном состоянии. Вот пример использования таймера:

//листинг 13
using System;
using System.WinForms;

class MyForm : Form {
	Button button1 = new Button();
	Button button2 = new Button();
	Timer timer1 = new Timer();

	public MyForm() {

		button1.Text = "Timer On";
		button1.Top = 16;
		button1.Left = 16;
		
		button2.Text = "Timer Off";
		button2.Top = 16;
		button2.Left = 96;

		this.Text = "Timer";
		this.Width = 200;
		this.Height = 88;

		timer1.Interval = 2000;

		button1.Click += new EventHandler(button1_Click);
		button2.Click += new EventHandler(button2_Click);
		timer1.Tick += new EventHandler(timer1_Tick);

		Controls.Add(button1);
		Controls.Add(button2);
	}//public MyForm()

	//обработчик события кнопки
	private void button1_Click(object sender, EventArgs e) {
		timer1.Start();
	}//private void button1_Click(object sender, EventArgs e)
	
	//обработчик события кнопки
	private void button2_Click(object sender, EventArgs e) {
		timer1.Stop();		
	}//private void button2_Click(object sender, EventArgs e)
	
	//обработчик события таймера
	private void timer1_Tick(object sender, EventArgs e) {
		MessageBox.Show("timer1_Tick");
	}//private void timer1_Tick(object sender, EventArgs e)

}//class MyForm : Form

class TestApp {
	public static void Main() {
		Application.Run(new MyForm());
	}//public static void Main()
}//class TestApp

На форме, расположены две кнопки: Timer On и Timer Off, которые включают и выключают таймер соответственно. При поступлении события от таймер на экран выводится диалоговое окно.

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

  • выбрать событие класса,
  • определить его делегат,
  • реализовать метод-обработчик, соответствующий делегату,
  • зарегистрировать обработчик события.

Подробное рассмотрение конкретных событий выходит за рамки стати, поэтому на этом все.

Особенности использования событий


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

//листинг 14
using System;

delegate void SimpleMessage(string msg);

//базовый класс, содержащий событие
class BaseEventClass {
	//событие
	public event SimpleMessage MyEvent;

	//метод, в котором размещен код генерации события
	public void RaiseEvent(string msg) {
		if (MyEvent!=null) MyEvent(msg); // код генерирующий событие
	}//public void RaiseEvent(string msg)

}//class BaseEventClass

//производный от BaseEventClass класс, наследует событие MyEvent
class DerivedEventClass : BaseEventClass {
	//метод, пытающийся сгенерировать событие базового класса
	public void UsingEvent() {	
		if (MyEvent!=null) MyEvent("Hello Subscriber");// ошибка!
	}//public void UsingEvent()
}//class DerivedEventClass : BaseEventClass


class Test {
	public static void Main() {
	}//public static void Main()
}//class Test

Несмотря на размер данного примера он достаточно прост. Класс BaseEventClass содержит событие. В методе RaiseEvent находится код для его генерации. Производный от BaseEventClass класс - DerivedEventClass в методе UsingEvent пытается сгенерировать событие базового класса, однако это ему не удастся, поскольку при компиляции кода выводится следующее сообщение об ошибке:

listing14.cs(22,7): error CS0070: The event 'BaseEventClass.MyEvent' 
can only appear on the left hand side of += or -= (except when used from 
within the class it is defined in)
listing14.cs(22,22): error CS0070: The event 'BaseEventClass.MyEvent' can
 only appear on the left hand side of += or -= (except when used from 
within the class it is defined in)
Перевод:
(Событие 'BaseEventClass.MyEvent' может употребляться только с левой 
стороны операторов += или -= (кроме тех случаев когда оно используется в 
пределах класса, в котором определено)

Как вы видите этих сообщений два, потому как в коде, генерирующем событие:

if (MyEvent!=null) MyEvent("Hello Subscriber");

два раза идентификатор события используется неверным образом. Тот же самый результат Вы получите, если попытаетесь поместить код, генерирующий событие в другом классе. Если же Вам необходимо генерировать событие из кода других классов (в том числе и производных), предоставьте открытый метод, подобный RaiseEvent.

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

//листинг 15
using System;

delegate void SimpleMessage(string msg);

//базовый класс, содержащий событие
class BaseEventClass {
	//событие
	public event SimpleMessage MyEvent;

	//метод, в котором размещен код для генерации события
	public void RaiseEvent(string msg) {
		if (MyEvent!=null) MyEvent(msg); // код генерирующий событие
	}//public void RaiseEvent(string msg)

}//class BaseEventClass

//производный от BaseEventClass класс, наследует событие MyEvent
class DerivedEventClass : BaseEventClass {

	// метод-обработчик события базового класса
	private void DerivedEventClass_MyEvent(string msg) {
		Console.WriteLine("DerivedEventClass_MyEvent: ",msg);
	}//private static void MyMethod1(string msg)

	//метод, в котором размещен код подписки на событие базового класса
	public void UsingEvent() {
		//код подписки на событие базового класса
		this.MyEvent += new SimpleMessage(this.DerivedEventClass_MyEvent);
	}//public void UsingEvent()
}//class DerivedEventClass : BaseEventClass


class Test {

	public static void Main() {
		DerivedEventClass objWithEvent = new DerivedEventClass();
		objWithEvent.UsingEvent();
		objWithEvent.RaiseEvent("Hello Subscriber");		
	}//public static void Main()
}//class Test

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

this.MyEvent += new SimpleMessage(this.DerivedEventClass_MyEvent);

Как Вы видите, используется текущий экземпляр объекта (this), а метод DerivedEventClass_MyEvent не статический. Однако обычно в таких ситуациях используются виртуальные методы: пример - переопределение виртуального метода OnPaint, класса RichControl в классе, производном от Form.

Почему возникает ошибка? Данный факт объяснить очень просто. Как вы помните, при определении события с именем MyEvent, компилятор определяет одноименное скрытое поле-делегат. Поскольку генерация события осуществляется посредством делегата, а делегат в свою очередь имеет модификатор доступа private, то в результате мы имеем доступ к делегату только из методов класса, в котором определено событие. Так как поддержка делегатов и событий в C# реализована на уровне синтаксиса, в нем существует специальное сообщение об ошибке CS0070, которое предоставляет более ясное пояснение, нежели ошибка нарушения доступа к скрытым членам класса, что наверняка сбило бы с толку программиста, не представляющего себе механизм работы событий, реализованный в CLR.

Вы спросите: <А почему бы не сделать поле-делегат защищенным (protected)? Тогда возможно было бы генерировать событие базового класса в производных> Этого не было сделано разработчиками языка из соображений строгого следования принципам инкапсуляции. Действительно метод RaiseEvent может быть гораздо сложнее нашей реализации: события могут возникать при определенном состоянии объекта, проверка которого должна осуществляться в самом классе. Вот вам один из возможных вариантов ответа. Если Вас не устраивает данный факт (что маловероятно) - не пользуйтесь ключевым словом event, а прибегните к собственной реализации механизма, обеспечивающего работу событий (рассмотренный выше).

Заключение


Механизм событий CLR построен на основе делегатов, поэтому умение хорошо работать с делегатами значительно облегчит Вам жизнь в практике программирования для .NET. В данной статье достаточно подробно были рассмотрены основы использования делегатов и событий, а так же объяснены принципы их реализации в CLR.

Александр Нестеренко

Другие разделы
C, C++
Java
PHP
VBasic, VBS
Delphi и Pascal
Новое в разделе
Базы данных
Общие вопросы
Теория программирования и алгоритмы
JavaScript и DHTML
Perl
Python
Active Server Pages
Программирование под Windows
I2R-Журналы
I2R Business
I2R Web Creation
I2R Computer
рассылки библиотеки +
И2Р Программы
Всё о Windows
Программирование
Софт
Мир Linux
Галерея Попова
Каталог I2R
Партнеры
Amicus Studio
NunDesign
Горящие путевки, идеи путешествийMegaTIS.Ru

2000-2008 г.   
Все авторские права соблюдены.
Rambler's Top100