Назад | Вперед

Глава 5 (продолжение 1)

Делегаты

К настоящему времени мы уже создали немало тестовых приложений. Практически
все приложения состояли из блока Main(), в котором создавались различные
объекты и им пересылались различные сообщения-команды.


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

При программировании под Windows на С++ основное средство для решения этой
проблемы - это функция обратного вызова (callback function) которая
основана на использовании указателей на функции в оперативной памяти.
При помощи этого средства программист может обеспечить возможность
обратного вызова (call back) одной функцией другой. Однако указатель на
функцию - это всего лишь адрес в оперативной памяти и из этого вытекает
множество неудобств и потенциальных ошибок, насколько было бы проще и
безопаснее, если вмеcто голого адреса у нас была бы какая то конструкция,
которая могла бы проверять при выполнении обратного вызова и количество
передаваемых параметров и их тип, и возвращаемое значение,
и следование определенной логике вызова...
Однако в традиционном С++
ничего подобного нет.

А в C# такое средство есть! Оно называется делегатом (delegate)
и выполняет те же действия, что и указатель на функцию, но гораздо более
безопасными и лучше соответствующими принципам ООП способами.
При создании делегата в C# указывается не только имя метода, но и набор
передаваемых функции параметров (если они есть), и возвращаемое функцией
значение. Как и все в мире C#, делегат - это специальный класс.

Любой делегат производится от единого базового класса - System. MulticastDelegate
с заранее определенным набором членов. Поэтому
когда мы создаем новый делегат, например вот так:

public delegate void PlayAcidHouse(object PaulOakenfold, int volume);

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

public class PlayAcidHouse :System.MulticastDelegate
{

PlayAcidHouse(object target, int ptr);

// Синхронный метод Invoke()
public void virtual Invoke(object PaulOakenfold, int volume);

//Асинхронная версия того же самого обратного вызова
public virtual IAsincResult BeginInvoke(object PaulOakenfold, int volume, AsyncCallback cb, object o);

public virtual void EndInvoke(AsyncResult result);

}

Тот класс, который был создан при создании делегата, содержит два открытых
метода Invoke() и BeginInvoke(), первый из которых предназначен для
синхронного вызова, а второй - для асинхронного. Пока мы рассмотрим
только средство для синхронного вызова MulticastDelegate

Вверх

Пример делегата

Чтобы рассмотреть применение делегата на практике, нам потребуется внести в
наш класс Car очередные изменения. В этот раз мы добавим две новые
переменные типа bool. Первая из них будет определять, является ли наш
автомобиль грязным (IsDirty), а вторая - нуждается ли автомобиль
в замене покрышек (shouldRotate).
Чтобы пользователю было проще взаимодействовать с новыми данными, в Car так
же будут добавлены дополнительные свойства и новый вариант конструктора:

public class Car
{

...

//Новые переменные!
private bool isDirty; //испачкан ли наш авто?
private bool shouldRotate; //нужна ли замена шин?

//Конструктор с новыми параметрами
public Car(string name, int max, int carr, bool dirty, bool rotate)
{

...
isDirty = dirty;
shouldRotate = rotate;
}

//Свойство для isDirty
public bool Dirty
{

get {return isDirty;}
set {isDirty = value;}
}

//Свойство для shouldRotate
public bool Rotate
{

get {return shouldRotate;}
set {shouldRotate = value;}
}
}

Теперь предположим, что мы объявили делегат в текущем пространстве имен (но не внутри класса Car)
следующим образом (вспомним что делегат - это не более чем
объектно-ориентированная надстройка, в основе которой - тот же указатель
на функцию):

//Делегат - это класс, инкапсулирующий указатель на функцию.В нашем случае
//этой функцией должен стать какой-то метод, принимающий в качестве
//параметра объект класса Car и ничего не возвращающий:

public delegate void CarDelegate(Car c);

Если мы рассмотрим наше приложение при помощи ILDasm.exe , то мы обнаружим новый класс CarDelegate, который является производным от MulticastDelegate

Подсказка

Рис 5.3 Делегат C# - это класс, производный от MulticastDelegate

Код программы в файле Prog05_10

Вверх

Делегаты как вложенные типы

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

//Помещаем определение делегата внутрь определения класса
public class Car : Object
{

//Теперь наш делегат получит служебное имя Car$carDelegate
//(то есть станет вложенным типом)
public delegate void CarDelegate(Car c);

...

}

Поскольку как мы выяснили делегат - это новый класс, производный от
System. MulticastDelegate, у нас получился вложенный класс CarDalegate .
Убедиться в этом можно при помощи ILDasm.exe(рис 5.4)

Подсказка

Рис 5.4 Вложенный делегат

Эта программа ILDasm.exe находится на моем компе в папке:
c:\tmp\Program Files\Microsoft Visual Studio.NET\FrameworkSDK\Bin\ildasm.exe

Я сначала запуская программу ILDasm.exe, затем через меню File/Open
открываю программу CarDelegate.exe. И вижу всю инф , отображенную на рис 5.4

Код программы в файле Prog05_11

Вверх

Члены System. MulticastDelegate

Конечно, делегат был создан как тип, производный от
System. MulticastDelegate, не потому, что так захотелось, а затем, чтобы
каждый делегат C# унаследовал набор полезных членов, предназначенных
для использования программистами. Наиболее интересные члены, которые
наследуются любыми делегатами от System. MulticastDelegate, представлены в таб 5.2

Таб 5.2 Некоторые унаследованные члены делегатов

Method Это свойство возвращает имя метода, на который указывает делегат
Target Если делегат указывает на метод-член класса, то этот член возвращает имя этого класса. Если Target возвращает значение типа null, то делегат указывает на статический метод.
Combine() Этот статический метод используется для создания делегата, указывающего на несколько разных функций.
GetInvocationList() Возвращает массив типов Delegate, каждый из которых представляет собой запись во внутреннем списке указателей на функции делегата.
Remove() Этот статический метод удаляет делегат из списка указателей на функции.

Многоадресный делегат (multicast delegate) позволяет указывать на любое
количество функций. При этом внутри делегата создается внутренний список
указателей на функции
. Поскольку все делегаты в C# производятся от
System.MulticastDelegate, то любой делегат C# потенциально является
многоадресным.
Чтобы добавить новый указатель на функцию во внутренний
список делегата, используется метод Combine() или перегруженный оператор
сложения +, а чтобы удалить указатель - метод Remove().

Вверх

Применение CarDelegate

Мы только что создали делегат, а значит создали указатель на функцию. Это
значит, что теперь мы сможем создавать новые функции, которые будут
принимать наш делегат в качестве параметра.
Лучше всего разобрать эту
ситуацию на примере.

Предположим, что у нас есть новый класс, который называется гараж (Garage).Этот класс
представляет собой набор объектов класса Car (для создания набора
используется тип arrayList). При создании объекта класса Garage, внутрь
этого объекта помещается несколько объектов класса Car.

Пусть наш класс Garage определяет открытый метод ProcessCars(), который
принимает единственный параметр - делегат Car.CarDelegate .
В определении метода ProcessCars() мы будем передавать все объекты Car из
набора в качестве параметра той функции, на которую указывает наш делегат.

Чтобы проиллюстрировать схему внутренней работы делегата, мы так же
воспользуемся возможностями двух унаследованных от System. MulticastDelegate
членов - Target и Method. Они нам будут нужны для того, чтобы определить
на какую именно функцию в настоящий момент указывает делегат.

А вот и определение класса Garage:

public class Garage
{

// Набор (список) машин в гараже.
ArrayList theCars = new ArrayList();

//создаем объекты машин в гараже (инициализируем список)
public Garage()
{

theCars.Add(new Car("Viper", 100, 0, true, false));
theCars.Add(new Car("Fred", 100, 0, false, false));
theCars.Add(new Car("BillyBob", 100, 0, false, true));
theCars.Add(new Car("Bart", 100, 0, true, true));
theCars.Add(new Car("Stan", 100, 0, false, true));
}

// Этот метод - ProcessCars() - принимает CarDelegate в качестве параметра.
// То есть метод принимает в качестве параметра - делегат.
//А делегат этот является вложенным в класс Car (был объявлен внутри класса)
// Можно считать, что 'proc' это эквивалент указателя на функцию...
//То есть наш делегат указывает на функцию через указатель 'proc' //

public void ProcessCars(Car.CarDelegate proc )
{

// Интересно, куда мы передаем наш вызов?
foreach (Delegate d in proc.GetInvocationList() )
{
Console.WriteLine("***** Calling: {0} *****", d.Method);
}

//Итак метод GetInvocationList() возвращает массив типа Delegate,
//а свойство Method возвращает имя функции,
//на которую указывает делегат (отдельный делегат массива делегатов).
//Кроме того свойство Method сообщит
//так же имя самого делегата и имя класса, в который он этот делегат вложен.
//Поскольку метод GetInvocationList() возвращает массив делегатов,
//то и используется цикл foreach, чтобы просмотреть все делегаты.
//В рассматриваемой нами программе всего один делегат.
//Поэтому произойдет только одна итерация цикла.

// Am I calling an object's method or a static method?
//еще одна проверка: вызываемый метод является статическим или обычным?
//На какой метод указывает делегат? Используем свойство Target делегата .
//Если делегат указывает на метод-член класса, то Target возвращает имя этого класса.
//Если Target возвращает значение типа null, то делегат указывает на статический метод.

if ( proc.Target != null)

Console.WriteLine("\n-->Target: {0}", proc.Target );
else
Console.WriteLine("\n-->Target is a static method");

// Now call method for each car.
//теперь наконец вызовем метод, на который указывает делегат
//для каждого автомобиля в списке theCars
//для чего это все затевалось: в цикле при помощи делегата вызываем метод
//на который указывает делегат и передаем ему объект Car

//То есть в цикле при помощи указателя на функцию вызываем функцию
//и передаем ей в в качестве параметра объект Car

foreach (Car c in theCars)
{

Console.WriteLine("\n-> Processing a Car");
proc(c);
}
Console.WriteLine();
}
}

При вызове метода ProcessCars, необходимо передать ему в качестве параметра
имя метода, который должен обработать данный вызов. Например пусть у нас
есть два статических метода - WashCar() (помыть машину) и RotateTires()
(заменить покрышки). Вызов этих методов через Car.CarDelegate может
выглядеть следующим образом:

public class CarApp
{

//Первый метод, на который будет указывать делегат
public static void WashCar(Car c)
{

if(c.Dirty)
Console.WriteLine("Cleaning a car");
else
Console.WriteLine("This car is already clean ...");
}

//Второй метод, на который будет указывать делегат
public static void RotateTires(Car c)
{

if(c.Rotate)
Console.WriteLine("Tires have been rotated");
else
Console.WriteLine(" Dont need to be rotated ...");
}

static void Main(string[] args)
{

// Make the garage.
Garage g = new Garage();

// Wash all dirty cars.
g.ProcessCars(new Car.CarDelegate(WashCar));

// Rotate the tires.
g.ProcessCars(new Car.CarDelegate(RotateTires));

Console.ReadLine();

}

}

То есть вызывается вложенный в класс Car делегат CarDelegate и ему
в качестве параметра передается функция (допустим WashCar()),
которая ничего не возвращает и принимает в качестве параметра
экземпляр класса Car. Интересный упрощенный синтаксис!

Обратите внимание, что два наших статич метода WashCar() и RotateTires()
в точности совпадают с сигнатурой, определенной делегатом (они принимают
один объект типа Car и возвращают значение типа void - то есть ничего не
возвращают). Когда мы передаем делегату имя функции, тем самым это имя
добавляется во внутренний список указателей на функции для делегата.

Результат работы функции показан ниже.Обратите внимание на те сообщения,
которые генерируются при помощи свойств Target и Method.

Результат работы программы:

***** Calling: Void WashCar(CarDelegate.Car) *****

-->Target is a static method

-> Processing a Car
Cleaning a car

-> Processing a Car
This car is already clean ...

-> Processing a Car
This car is already clean ...

-> Processing a Car
Cleaning a car

-> Processing a Car
This car is already clean ...

***** Calling: Void RotateTires(CarDelegate.Car) *****

-->Target is a static method

-> Processing a Car
Dont need to be rotated ...

-> Processing a Car
Dont need to be rotated ...

-> Processing a Car
Tires have been rotated

-> Processing a Car
Tires have been rotated

-> Processing a Car
Tires have been rotated

Обратите внимание на те сообщения. которые генерируются при помощи свойств
Target и Method.

Код программы в файле Prog05_12

Вверх

Анализ работы делегата.

В нашем примере работа программ начинается с создания экземпляра объекта
класса Garage в функции Main().

// Make the garage.
Garage g = new Garage();

Далее при помощи делегата этот объект передает всю работу
двум статическим функциям - WashCar и RotateTires.
Таким образом. если мы пишем :

// Вымыть все грязные машины
g.ProcessCars(new Car.CarDelegate(WashCar));

в действительности мы указываем "Добавить указатель на функцию WashCar
во внутреннюю таблицу указателей делегата Car.CarDelegate и передать
этот делегат функции ProcessCars() класса Garage". Здесь реальную часть работы
выполняет другая часть системы (которая потом будет объяснять нам, почему
замена масла, на которую нужно было потратить 30 минут, заняла два часа).
Таким образом, функция ProcessCars() реально работает следующим образом:

//CarDelegate уже указывает на функция WashCar
public void ProcessCars(Car.CarDelegate proc)
{

foreach (Car c in the Cars)
{
proc(c); // proc(c) => CarApp.WashCar(c)
}
...
}

То же самое можно сказать и в отношении второго метода:

// Поменять шины
g.ProcessCars(new Car.CarDelegate(RotateTires));

//Теперь CarDelegate указывает на функцию RotateTires
public void ProcessCars(Car.CarDelegate proc)
{

foreach (Car c in the Cars)
{
proc(c); // proc(c) => CarApp.RotateTires(c)
}
...
}

Обратите так же внимание, что при вызове ProcessCars() мы обязаны создать
и объект делегата при помощи ключевого слова new:

// Вымыть все грязные машины
g.ProcessCars(new Car.CarDelegate(WashCar));

// Поменять шины
g.ProcessCars(new Car.CarDelegate(RotateTires));

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

Да как видим многое делается автоматически (за спиной программиста),
в результате синтаксис весьма упрощен!

Вверх

Многоадресность

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

static int Main(string[] args)
{

// Make the garage.
Garage g = new Garage();

//создадим два новых делегата

// Wash all dirty cars.
Car.CarDelegate wash = new Car.CarDelegate(WashCar);

// Rotate the tires.
Car.CarDelegate rotate = new Car.CarDelegate(RotateTires));

// Чтобы объединить два указателя на функции в многоадресном делегате,
//используется перегруженны оператор сложения (+).
//В результате создается новый делегат,
//который содержит указатели на обе функции.

g.ProcessCars(wash + rotate);
return 0;

Console.ReadLine();

}

В этом примере вначале мы создали два отдельных делегата, каждый с
указателем на свою функцию. При вызове ProcessCars() этому методу в
действительности передается новый делегат, который содержит все
указатели на функцию из двух предыдущих делегатов.
Оператор + - это
просто более удобный вариант статического метода Delegate.Combine()
(см таб 5.2)
Combine() - этот статический метод используется для создания делегата,
указывающего на несколько разных функций

Результат работы программы:

***** Calling: Void WashCar(CarDelegate.Car) *****
***** Calling: Void RotateTires(CarDelegate.Car) *****

-->Target is a static method

-> Processing a Car
Cleaning a car
Dont need to be rotated ...

-> Processing a Car
This car is already clean ...
Dont need to be rotated ...

-> Processing a Car
This car is already clean ...
Tires have been rotated

-> Processing a Car
Cleaning a car
Tires have been rotated

-> Processing a Car
This car is already clean ...
Tires have been rotated

Код программы в файле Prog05_13

Таким образом тот же самый код может выглядеть так:

//Оператор + можно использовать вместо метода Combine
g.ProcessCars((Car.CarDelegate)Delegate.Combine(wash, rotate));

Результат работы программы будет тот же, что и предыдущий.
Код программы в Prog05_14

Если мы хотим, чтобы наш комбинированный делегат не исчез после выполнения
метода ProcessCars(), а остался в боевой готовности для последующего
использования, его можно создать и за пределами ProcessCars() :

// Создаем два новых делегата
Car.CarDelegate wash = new Car.CarDelegate(WashCar);
Car.CarDelegate rotate = new Car.CarDelegate(RotateTires));

//Объединим их в новый делегат, который теперь можно использовать где угодно
MulticastDelegate d = wash + rotate;

//Передаем комбинированный делегат методу ProcessCars()
g.ProcessCars((Car.CarDelegate)d);

Результат работы программы тот же.
Код программы в файле Prog05_15

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

Первый параметр метода Remove() определяет делегат, с
которым производится операция. а второй - тот указатель, который должен
быть удален:

//Статический метод Remove() возвращает новый делегат - с удаленной записью
//в таблице указателей на функции
Delegate washOnly = MulticastDelegate.Remove(d, rotate);
g.ProcessCars((CarDelegate)washOnly);

Перед тем, как запустить полученную программу на выполнение, мы вначале
обновим ProcessCars таким образом, чтобы при помощи метода
Delegate.GetInvocationlist() вывести на консоль все указатели на
функции, хранящиеся во внутренней таб. Этот метод возвращает массив
объектов Delegate, которые мы выведем на консоль, используя конструкцию foreach:

public void ProcessCars(CarDelegate proc)
{

//куда мы передаем вызов?
foreach(Delegate d in proc.GetInvocationList())
{
Console.WriteLine("***** Calling " + d.Method.ToString() + " *****\n");
}
...

}

Результат выполнения программы:

***** Calling Void WashCar(CarDelegate.Car) *****
***** Calling Void RotateTires(CarDelegate.Car) *****

-->Target is a static method


-> Processing a Car
Cleaning a car
Dont need to be rotated ...

-> Processing a Car
This car is already clean ...
Dont need to be rotated ...

-> Processing a Car
This car is already clean ...
Tires have been rotated

-> Processing a Car
Cleaning a car
Tires have been rotated

-> Processing a Car
This car is already clean ...
Tires have been rotated

***** Calling Void WashCar(CarDelegate.Car) *****

-->Target is a static method

-> Processing a Car
Cleaning a car

-> Processing a Car
This car is already clean ...

-> Processing a Car
This car is already clean ...

-> Processing a Car
Cleaning a car

-> Processing a Car
This car is already clean ...

Код программы в файле Prog05_16

Вверх

Делегаты, указывающие на обычные функции

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

//Статические функции перестали быть статическими и переместились во
//вспомогательный класс

public class ServiceDept
{

//Уже не статическая!
public void WashCar(Car c)
{
if(c.Dirty)
Console.WriteLine("Cleaning a car");
else
Console.WriteLine("This car is already clean ...");
}

//То же самое
public void RotateTires(Car c)
{

if(c.Rotate)
Console.WriteLine("Tires have been rotated");
else
Console.WriteLine(" Dont need to be rotated ...");
}

}

Теперь мы можем обновить наше приложение:

public static int Main(string[] args)
{

// Make the garage.
Garage g = new Garage();

// Make the service department.
ServiceDept sd = new ServiceDept();

// Wash all dirty cars.
Car.CarDelegate wash = new Car.CarDelegate(sd.WashCar);

// Rotate the tires.
Car.CarDelegate rotate = new Car.CarDelegate(sd.RotateTires);

MulticastDelegate d = wash + rotate;

g.ProcessCars(Car.CarDelegate)d);

return 0;

Console.ReadLine();

}

Результат работы программы:
Обратите внимание на имя вызываемого метода.

***** Calling Void RotateTires(CarDelegate.Car) *****

-->Target: CarDelegate.ServiceDept

-> Processing a Car
Cleaning a car
Dont need to be rotated ...

-> Processing a Car
This car is already clean ...
Dont need to be rotated ...

-> Processing a Car
This car is already clean ...
Tires have been rotated

-> Processing a Car
Cleaning a car
Tires have been rotated

-> Processing a Car
This car is already clean ...
Tires have been rotated

Код программы в файле Prog05_17

Код приложения CarDelegate можно найти в подкаталоге Chapter 5.



Назад | Вверх | Вперед