Тверской государственный университет
Опубликован: 22.11.2005 | Доступ: свободный | Студентов: 30353 / 1814 | Оценка: 4.31 / 3.69 | Длительность: 28:26:00
ISBN: 978-5-9556-0050-5
Лекция 20:

Функциональный тип в C#. Делегаты

< Лекция 19 || Лекция 20: 12345 || Лекция 21 >

Операции над делегатами. Класс Delegate

Давайте просуммируем то, что уже известно о функциональном типе данных. Ключевое слово delegate позволяет задать определение функционального типа (класса), фиксирующее контракт, которому должны удовлетворять все функции, принадлежащие классу. Функциональный класс можно рассматривать как ссылочный тип, экземпляры которого являются ссылками на функции. Заметьте, ссылки на функции - это безопасные по типу указатели, которые ссылаются на функции с жестко фиксированной сигнатурой, заданной делегатом. Следует также понимать, что это не простая ссылка на функцию. В том случае, когда экземпляр делегата инициирован динамическим методом, то экземпляр хранит ссылку на метод и на объект X, вызвавший этот метод.

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

Delegate FType = new Delegate(<определение типа>)

Но так объявлять переменные этого класса нельзя, и стоит понять, почему. Есть ли вообще класс Delegate? Ответ положителен - есть такой класс. При определении функционального типа, например:

public delegate int FType(int X);

переменная FType принадлежит классу Delegate. Почему же ее нельзя объявить привычным образом? Дело не только в синтаксических особенностях этого класса. Дело в том, что класс Delegate является абстрактным классом. Вот его объявление:

public abstract class Delegate: ICloneable, ISerializable

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

Трудно, кажется, придумать, что можно делать с делегатами. Однако, у них есть одно замечательное свойство - их можно комбинировать. Представьте себе, что существует список работ, которые нужно выполнять, в зависимости от обстоятельств, в разных комбинациях. Если функции, выполняющие отдельные работы, принадлежат одному классу, то для решения задачи можно использовать делегатов и использовать технику их комбинирования. Замечу, что возможность комбинирования делегатов появилась, в первую очередь, для поддержания работы с событиями. Когда возникает некоторое событие, то сообщение о нем посылается разным объектам, каждый из которых по-своему обрабатывает событие. Реализуется эта возможность на основе комбинирования делегатов.

В чем суть комбинирования делегатов? Она прозрачна. К экземпляру делегату разрешается поочередно присоединять другие экземпляры делегата того же типа. Поскольку каждый экземпляр хранит ссылку на функцию, то в результате создается список ссылок. Этот список называется списком вызовов (invocation list). Когда вызывается экземпляр, имеющий список вызова, то поочередно, в порядке присоединения, начинают вызываться и выполняться функции, заданные ссылками. Так один вызов порождает выполнение списка работ.

Понятно, что, если есть операция присоединения делегатов, то должна быть и обратная операция, позволяющая удалять делегатов из списка.

Рассмотрим основные методы и свойства класса Delegate. Начнем с двух статических методов - Combine и Remove. Первый из них присоединяет экземпляры делегата к списку, второй - удаляет из списка. Оба метода имеют похожий синтаксис:

Combine(del1, del2)
Remove(del1, del2)

Аргументы del1 и del2 должны быть одного функционального класса. При добавлении del2 в список, в котором del2 уже присутствует, будет добавлен второй экземпляр. При попытке удаления del2 из списка, в котором del2 нет, Remove благополучно завершит работу, не выдавая сообщения об ошибке.

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

del1 = (<type>) Combine(del1, del2);
del1 = (<type>) Remove(del1, del2);

Метод GetInvocationList является динамическим методом класса - он возвращает список вызовов экземпляра, вызвавшего метод. Затем можно устроить цикл foreach, поочередно получая элементы списка. Чуть позже появится пример, поясняющий необходимость подобной работы со списком.

Два динамических свойства Method и Target полезны для получения подробных сведений о делегате. Чаще всего они используются в процессе отражения, когда делегат поступает извне и необходима метаинформация, поставляемая с делегатом. Свойство Method возвращает объект класса MethodInfo из пространства имен Reflection. Свойство Target возвращает информацию об объекте, вызвавшем делегата, в тех случаях, когда делегат инициируется не статическим методом класса, а динамическим, связанным с вызвавшим его объектом.

У класса Delegate, помимо методов, наследуемых от класса Object, есть еще несколько методов, но мы на них останавливаться не будем, они используются не столь часто.

Операции "+" и "-"

Наряду с методами, над делегатами определены и две операции: "+" и "-", которые являются более простой формой записи добавления делегатов в список вызовов и удаления из списка. Операции заменяют собой методы Combine и Remove. Выше написанные присваивания объекту del1 с помощью этих операций могут быть переписаны в виде:

del1 +=del2;
del1 -=del2;

Как видите, запись становится проще, исчезает необходимость в задании явного приведения к типу. Ограничения на del1 и del2, естественно, остаются те же, что и для методов Combine и Remove.

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

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

Начнем с построения класса с именем Combination, где, следуя уже описанной технологии, введем делегатов как закрытые свойства, доступ к которым идет через процедуру-свойство get. Три делегата одного класса будут описывать действия трех городских служб. Класс будет описываться ранее введенным делегатом MesToPers, размещенным в пространстве имен проекта. Вот программный код, в котором описаны функции, задающие действия служб:

class Combination
{
	private static void policeman(string mes)
	{
		//анализ сообщения
		if(mes =="Пожар!")
			Console.WriteLine(mes + " Милиция ищет виновных!");
		else	
			Console.WriteLine(mes +" Милиция здесь!");
	}
	private static void ambulanceman(string mes)
	{
		if(mes =="Пожар!")
			Console.WriteLine(mes + " Скорая спасает пострадавших!");
		else
			Console.WriteLine(mes + " Скорая помощь здесь!");
	}
	private static void fireman(string mes)
	{
		if(mes =="Пожар!")
			Console.WriteLine(mes + " Пожарные тушат пожар!");
		else
			Console.WriteLine( mes + " Пожарные здесь!");
	}
}

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

public static MesToPers Policeman
{
	get {return (new MesToPers(policeman));}
}
public static MesToPers Fireman
{
	get {return (new MesToPers(fireman));}
}
public static MesToPers Ambulanceman
{
	get {return (new MesToPers(ambulanceman));}
}

Три статических открытых свойства - Policeman, Fireman, Ambulanceman - динамически создают экземпляры класса MesToPers, связанные с соответствующими закрытыми функциями класса.

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

public void TestSomeServices()
{
	MesToPers Comb;
	Comb = (MesToPers)Delegate.Combine(Combination.Ambulanceman,
			Combination.Policeman);
	Comb = (MesToPers)Delegate.Combine(Comb,Combination.Fireman);
			Comb("Пожар!");

Вначале объявляется без инициализации функциональная переменная Comb, которой в следующем операторе присваивается ссылка на экземпляр делегата, созданного методом Combine, чей список вызова содержит ссылки на экземпляры делегатов Ambulanceman и Policeman. Затем к списку вызовов экземпляра Comb присоединяется новый кандидат Fireman. При вызове делегата Comb ему передается сообщение "Пожар!". В результате вызова Comb поочередно запускаются все три экземпляра входящие в список, каждому из которых передается сообщение.

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

Comb = (MesToPers)Delegate.Remove(Comb,Combination.Policeman);
//Такое возможно: попытка отключить не существующий элемент
Comb = (MesToPers)Delegate.Remove(Comb,Combination.Policeman);
Comb("Через 30 минут!");
Comb = (MesToPers)Delegate.Remove(Comb,Combination.Ambulanceman);
Comb("Через час!");
Comb = (MesToPers)Delegate.Remove(Comb,Combination.Fireman);
//Comb("Через два часа!"); // Comb не определен

В этом фрагменте поочередно отключаются разные службы - милиция, скорая помощь, пожарные, и каждый раз вызывается Comb. После последнего отключения, когда список вызовов становится пустым, вызов Comb приводит к ошибке, потому оператор вызова закомментирован.

Покажем теперь, что ту же работу можно выполнить, используя не методы, а операции:

//операции + и -
			Comb = Combination.Ambulanceman;
			Console.WriteLine( Comb.Method.Name);
			Comb+= Combination.Fireman;
			Comb+= Combination.Policeman;
			Comb("День города!");
			Comb -= Combination.Ambulanceman;
			Comb -= Combination.Fireman;
			Comb("На следующий день!");
		}//TestSomeServices

Обратите внимание, здесь демонстрируется вызов свойства Method, возвращающее объект, свойство Name которого выводится на печать. Результаты, порожденные работой этой процедуры, изображены на рис. 20.6.

Службы города

Рис. 20.6. Службы города

Пример "Плохая служба"

Как быть, если в списке вызовов есть "плохой" экземпляр, при вызове которого возникает ошибка, приводящая к выбрасыванию исключительной ситуации? Тогда стоящие за ним в очереди экземпляры не будут вызваны, хотя они вполне могли бы выполнить свою часть работы. В этом случае полезно использовать метод GetInvocationList и в цикле поочередно вызывать делегатов. Вызов делегата следует поместить в охраняемый блок, тогда при возникновении исключительной ситуации в обработчике ситуации можно получить и выдать пользователю всю информацию о нарушителе, а цикл продолжит выполнение очередных делегатов из списка вызова.

Добавим в класс Combination "плохого" кандидата, который пытается делить на ноль:

//метод, вызывающий исключительную ситуацию
public static void BadService(string mes)
{
	int i =7, j=5, k=0;
	Console.WriteLine("Bad Service: Zero Divide");
	j=i/k;
}

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

public void TestBadJob()
{
	MesToPers Comb;
	Comb = (MesToPers)Delegate.Combine(Combination.Ambulanceman,
		Combination.Policeman);
	Comb = (MesToPers)Delegate.Combine(Comb,
		new MesToPers(Combination.BadService));
	Comb = (MesToPers)Delegate.Combine(Comb,Combination.Fireman);
	foreach(MesToPers currentJob in Comb.GetInvocationList())
	{
		try
		{
			currentJob("Пожар!");
		}
		catch(Exception e)
		{
			Console.WriteLine(e.Message);
			Console.WriteLine(currentJob.Method.Name);
		}
	}
}//BadJob

Поясню, как будет работать эта процедура при ее вызове. Вначале две службы нормально отработают, но при вызове третьей службы возникнет исключительная ситуация "деление на ноль". Универсальный обработчик Exception перехватит эту ситуацию и напечатает как свойство Message объекта e, так и имя метода, вызвавшего исключительную ситуацию, используя свойство Method объекта, вызвавшего ситуацию. После завершения работы блока обработчика ситуации выполнение программы продолжится, выполнится следующий шаг цикла, и служба пожарных благополучно выполнит свою работу. Вот результаты вывода:

"Плохая служба"

Рис. 20.7. "Плохая служба"

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

< Лекция 19 || Лекция 20: 12345 || Лекция 21 >
Александр Галабудник
Александр Галабудник

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

Александра Гусева
Александра Гусева