Глава 5 (продолжение 1)
К настоящему времени мы уже создали немало тестовых приложений. Практически
все приложения состояли из блока Main(), в котором создавались различные
объекты и им пересылались различные сообщения-команды.
При программировании под 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 virtual void EndInvoke(AsyncResult result);
по созданию нового класса:
{
public void virtual Invoke(object PaulOakenfold, int volume);
public virtual IAsincResult BeginInvoke(object PaulOakenfold, int volume, AsyncCallback cb, object o);
Тот класс, который был создан при создании делегата, содержит два открытых
метода Invoke() и BeginInvoke(), первый из которых предназначен для
синхронного вызова, а второй - для асинхронного. Пока мы рассмотрим
только средство для синхронного вызова MulticastDelegate
Вверх
Чтобы рассмотреть применение делегата на практике, нам потребуется внести в
//Новые переменные!
//Конструктор с новыми параметрами
//Свойство для isDirty
//Свойство для shouldRotate
Теперь предположим, что мы объявили делегат в текущем пространстве имен (но не внутри класса Car)
public delegate void CarDelegate(Car c);
Если мы рассмотрим наше приложение при помощи ILDasm.exe , то мы обнаружим новый класс CarDelegate, который является производным от MulticastDelegate
Рис 5.3 Делегат C# - это класс, производный от MulticastDelegate
Код программы в файле Prog05_10
Вверх
Сейчас созданный нами делегат существует отдельно от логически связанного
...
Поскольку как мы выяснили делегат - это новый класс, производный от
Рис 5.4 Вложенный делегат
Эта программа ILDasm.exe находится на моем компе в папке:
Я сначала запуская программу ILDasm.exe, затем через меню File/Open
Код программы в файле Prog05_11
Вверх
Конечно, делегат был создан как тип, производный от
Таб 5.2 Некоторые унаследованные члены делегатов
Пример делегата
наш класс 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;
public bool Dirty
{
set {isDirty = value;}
public bool Rotate
{
set {shouldRotate = value;}
следующим образом (вспомним что делегат - это не более чем
объектно-ориентированная надстройка, в основе которой - тот же указатель
на функцию):
//Делегат - это класс, инкапсулирующий указатель на функцию.В нашем случае
//этой функцией должен стать какой-то метод, принимающий в качестве
//параметра объект класса Car и ничего не возвращающий:
Делегаты как вложенные типы
с ним типа Car (оба они определены непосредственно в пространстве
имен). Однако делегат можно поместить и непосредственно внутрь
определения класса Car:
//Помещаем определение делегата внутрь определения класса
public class Car : Object
{
//(то есть станет вложенным типом)
public delegate void CarDelegate(Car c);
System. MulticastDelegate, у нас получился вложенный класс CarDalegate .
Убедиться в этом можно при помощи ILDasm.exe(рис 5.4)
c:\tmp\Program Files\Microsoft Visual Studio.NET\FrameworkSDK\Bin\ildasm.exe
открываю программу CarDelegate.exe.
И вижу всю инф , отображенную на рис 5.4
Члены System. MulticastDelegate
System. MulticastDelegate, не потому, что так захотелось, а затем, чтобы
каждый делегат C# унаследовал набор полезных членов, предназначенных
для использования программистами. Наиболее интересные члены, которые
наследуются любыми делегатами от System. MulticastDelegate, представлены в таб 5.2
Method | Это свойство возвращает имя метода, на который указывает делегат |
Target | Если делегат указывает на метод-член класса, то этот член возвращает имя этого класса. Если Target возвращает значение типа null, то делегат указывает на статический метод. |
Combine() | Этот статический метод используется для создания делегата, указывающего на несколько разных функций. |
GetInvocationList() | Возвращает массив типов Delegate, каждый из которых представляет собой запись во внутреннем списке указателей на функции делегата. |
Remove() | Этот статический метод удаляет делегат из списка указателей на функции. |
Многоадресный делегат (multicast delegate) позволяет указывать на любое
количество функций. При этом внутри делегата создается внутренний список
указателей на функции.
Поскольку все делегаты в C# производятся от
System.MulticastDelegate, то любой делегат C# потенциально является
многоадресным.
Чтобы добавить новый указатель на функцию во внутренний
список делегата, используется метод Combine() или перегруженный оператор
сложения +, а чтобы удалить указатель - метод Remove().
Вверх
Мы только что создали делегат, а значит создали указатель на функцию. Это
Предположим, что у нас есть новый класс, который называется гараж (Garage).Этот класс
Пусть наш класс Garage определяет открытый метод ProcessCars(), который
Чтобы проиллюстрировать схему внутренней работы делегата, мы так же
А вот и определение класса Garage:
public class Garage
//создаем объекты машин в гараже (инициализируем список)
// Этот метод - ProcessCars() - принимает CarDelegate в качестве параметра.
public void ProcessCars(Car.CarDelegate
proc
)
//Итак метод GetInvocationList() возвращает массив типа Delegate,
// Am I calling an object's method or a static method?
if (
proc.Target
!= null)
// Now call method for each car.
foreach (Car c in theCars)
При вызове метода ProcessCars, необходимо передать ему в качестве параметра
//Первый метод, на который будет указывать делегат
//Второй метод, на который будет указывать делегат
static void Main(string[] args)
// Wash all dirty cars.
// Rotate the tires.
Console.ReadLine();
То есть вызывается вложенный в класс Car делегат CarDelegate и ему
Обратите внимание, что два наших статич метода WashCar() и RotateTires()
Результат работы функции показан ниже.Обратите внимание на те сообщения,
Результат работы программы:
***** Calling: Void WashCar(CarDelegate.Car) *****
-->Target is a static method
-> Processing a Car
-> Processing a Car
-> Processing a Car
-> Processing a Car
-> Processing a Car
***** Calling: Void RotateTires(CarDelegate.Car) *****
-->Target is a static method
-> Processing a Car
-> Processing a Car
-> Processing a Car
-> Processing a Car
-> Processing a Car
Обратите внимание на те сообщения. которые генерируются при помощи свойств
Код программы в файле Prog05_12
Вверх
В нашем примере работа программ начинается с создания экземпляра объекта
Далее при помощи делегата этот объект передает всю работу
в действительности мы указываем "Добавить указатель на функцию WashCar
То же самое можно сказать и в отношении второго метода:
//Теперь CarDelegate указывает на функцию RotateTires
Обратите так же внимание, что при вызове ProcessCars() мы обязаны создать
// Поменять шины
Если представлять себе делегат только как указатель на функцию, то это может
Да как видим многое делается автоматически (за спиной программиста),
Вверх
Многоадресный делегат как уже говорилось - это объект, который может
//создадим два новых делегата
// Wash all dirty cars.
// Rotate the tires.
// Чтобы объединить два указателя на функции в многоадресном делегате,
g.ProcessCars(wash + rotate);
Console.ReadLine();
В этом примере вначале мы создали два отдельных делегата, каждый с
Результат работы программы:
***** Calling: Void WashCar(CarDelegate.Car) *****
-->Target is a static method
-> Processing a Car
-> Processing a Car
-> Processing a Car
-> Processing a Car
-> Processing a Car
Код программы в файле Prog05_13
Таким образом тот же самый код может выглядеть так:
Результат работы программы будет тот же, что и предыдущий.
Если мы хотим, чтобы наш комбинированный делегат не исчез после выполнения
//Объединим их в новый делегат, который теперь можно использовать где угодно
//Передаем комбинированный делегат методу ProcessCars()
Результат работы программы тот же.
Первый параметр метода Remove() определяет делегат, с
Перед тем, как запустить полученную программу на выполнение, мы вначале
Результат выполнения программы:
***** Calling Void WashCar(CarDelegate.Car) *****
-->Target is a static method
-> Processing a Car
-> Processing a Car
-> Processing a Car
-> Processing a Car
***** Calling Void WashCar(CarDelegate.Car) *****
-->Target is a static method
-> Processing a Car
-> Processing a Car
-> Processing a Car
-> Processing a Car
-> Processing a Car
Код программы в файле Prog05_16
Вверх
Во всех примерах, которые были приведены выше, делегаты указывали только на
public class ServiceDept
//То же самое
Теперь мы можем обновить наше приложение:
// Make the service department.
// Wash all dirty cars.
// Rotate the tires.
MulticastDelegate d = wash + rotate;
g.ProcessCars(Car.CarDelegate)d);
return 0;
Console.ReadLine();
Результат работы программы:
***** Calling Void RotateTires(CarDelegate.Car) *****
-->Target: CarDelegate.ServiceDept
-> Processing a Car
-> Processing a Car
-> Processing a Car
-> Processing a Car
-> Processing a Car
Код программы в файле Prog05_17
Код приложения CarDelegate можно найти в подкаталоге Chapter 5.
Применение CarDelegate
значит, что теперь мы сможем создавать новые функции, которые будут
принимать наш делегат в качестве параметра. Лучше всего разобрать эту
ситуацию на примере.
представляет собой набор объектов класса Car (для создания набора
используется тип arrayList). При создании объекта класса Garage, внутрь
этого объекта помещается несколько объектов класса Car.
принимает единственный параметр - делегат Car.CarDelegate .
В определении метода ProcessCars() мы будем передавать все объекты Car из
набора в качестве параметра той функции, на которую указывает наш делегат.
воспользуемся возможностями двух унаследованных от System. MulticastDelegate
членов - Target и Method. Они нам будут нужны для того, чтобы определить
на какую именно функцию в настоящий момент указывает делегат.
{
ArrayList theCars = new ArrayList();
public Garage()
{
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));
//
То есть метод принимает в качестве параметра - делегат.
//А делегат этот является вложенным в класс Car (был объявлен внутри класса)
// Можно считать, что 'proc' это эквивалент указателя на функцию...
//То есть наш делегат указывает на функцию через указатель 'proc'
//
{
foreach (Delegate d in
proc.GetInvocationList()
)
{
//а свойство Method возвращает имя функции,
//на которую указывает делегат (отдельный делегат массива делегатов).
//Кроме того свойство Method сообщит
//так же имя самого делегата и имя класса, в который он этот делегат вложен.
//Поскольку метод GetInvocationList() возвращает массив делегатов,
//то и используется цикл foreach, чтобы просмотреть все делегаты.
//В рассматриваемой нами программе всего один делегат.
//Поэтому произойдет только одна итерация цикла.
//еще одна проверка: вызываемый метод является статическим или обычным?
//На какой метод указывает делегат? Используем свойство Target делегата .
//Если делегат указывает на метод-член класса, то Target возвращает имя этого класса.
//Если Target возвращает значение типа null, то делегат указывает на статический метод.
//теперь наконец вызовем метод, на который указывает делегат
//для каждого автомобиля в списке theCars
//для чего это все затевалось: в цикле при помощи делегата вызываем метод
//на который указывает делегат и передаем ему объект Car
//То есть в цикле при помощи указателя на функцию вызываем функцию
//и передаем ей в в качестве параметра объект Car
{
proc(c);
Console.WriteLine();
имя метода, который должен обработать данный вызов. Например пусть у нас
есть два статических метода - WashCar() (помыть машину) и RotateTires()
(заменить покрышки). Вызов этих методов через Car.CarDelegate может
выглядеть следующим образом:
public class CarApp
{
public static void WashCar(Car c)
{
public static void RotateTires(Car c)
{
{
Garage g = new Garage();
g.ProcessCars(new Car.CarDelegate(WashCar));
g.ProcessCars(new Car.CarDelegate(RotateTires));
в качестве параметра передается функция (допустим WashCar()),
которая ничего не возвращает и принимает в качестве параметра
экземпляр класса Car. Интересный упрощенный синтаксис!
в точности совпадают с сигнатурой, определенной делегатом (они принимают
один объект типа Car и возвращают значение типа void - то есть ничего не
возвращают). Когда мы передаем делегату имя функции, тем самым это имя
добавляется во внутренний список указателей на функции для делегата.
которые генерируются при помощи свойств Target и Method.
Cleaning a car
This car is already clean ...
This car is already clean ...
Cleaning a car
This car is already clean ...
Dont need to be rotated ...
Dont need to be rotated ...
Tires have been rotated
Tires have been rotated
Tires have been rotated
Target и Method.
Анализ работы делегата.
класса Garage
в функции Main().
// Make the garage.
Garage g = new Garage();
двум статическим функциям -
WashCar и RotateTires.
Таким образом. если мы пишем :
// Вымыть все грязные машины
g.ProcessCars(new Car.CarDelegate(WashCar));
во внутреннюю таблицу указателей делегата Car.CarDelegate и передать
этот делегат функции ProcessCars() класса Garage". Здесь реальную часть работы
выполняет другая часть системы (которая потом будет объяснять нам, почему
замена масла, на которую нужно было потратить 30 минут, заняла два часа).
Таким образом, функция ProcessCars() реально работает следующим образом:
//CarDelegate уже указывает на функция WashCar
public void ProcessCars(Car.CarDelegate proc)
{
{
...
// Поменять шины
g.ProcessCars(new Car.CarDelegate(RotateTires));
public void ProcessCars(Car.CarDelegate proc)
{
{
...
и объект делегата при помощи ключевого слова new:
// Вымыть все грязные машины
g.ProcessCars(new Car.CarDelegate(WashCar));
g.ProcessCars(new Car.CarDelegate(RotateTires));
показаться странным. Однако вспомним, что делегат - это на самом деле
настоящий класс, производный от класса System.MulticastDelegate,
и в этом свете создание нового объекта этого класса не должно вызывать
удивления.
в результате синтаксис весьма упрощен!
Многоадресность
содержать в себе сразу несколько указателей на функции. В нашем примере
мы не использовали такую возможность, а вместо этого два раза создавали
делегат, каждый раз указываю лишь на одну функцию. Однако мы можем
воспользоваться и многоадресностью:
static int Main(string[] args)
{
Garage g = new Garage();
Car.CarDelegate wash = new Car.CarDelegate(WashCar);
Car.CarDelegate rotate = new Car.CarDelegate(RotateTires));
//используется перегруженны оператор сложения (+).
//В результате создается новый делегат,
//который содержит указатели на обе функции.
return 0;
указателем на свою функцию.
При вызове ProcessCars() этому методу в
действительности передается новый делегат, который содержит все
указатели на функцию из двух предыдущих делегатов.
Оператор + - это
просто более удобный вариант статического метода Delegate.Combine()
(см таб 5.2)
Combine() - этот статический метод используется для создания делегата,
указывающего на несколько разных функций
***** Calling: Void RotateTires(CarDelegate.Car) *****
Cleaning a car
Dont need to be rotated ...
This car is already clean ...
Dont need to be rotated ...
This car is already clean ...
Tires have been rotated
Cleaning a car
Tires have been rotated
This car is already clean ...
Tires have been rotated
//Оператор + можно использовать вместо метода 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;
g.ProcessCars((Car.CarDelegate)d);
Код программы в файле Prog05_15
необходимо запомнить главное: добавление во внутреннюю таб нового
указателя на функцию производится при помощи метода Combine() (или
перегруженного оператора +), а удаление - при помощи статического
метода 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())
{
...
***** Calling Void RotateTires(CarDelegate.Car) *****
-> Processing a Car
Cleaning a car
Dont need to be rotated ...
This car is already clean ...
Dont need to be rotated ...
This car is already clean ...
Tires have been rotated
Cleaning a car
Tires have been rotated
This car is already clean ...
Tires have been rotated
Cleaning a car
This car is already clean ...
This car is already clean ...
Cleaning a car
This car is already clean ...
Делегаты, указывающие на обычные функции
статические функции. Однако это совершенно не обязательно - мы вполне
можем создавать делегаты, указывающие на самые обычные функции. вызов
которых производится через объект класса. Для того, чтобы показать это
на примере, мы переместим наши функции WashCar() и RotateTires() в новый
класс ServiceDept (отдел обслуживания):
//Статические функции перестали быть статическими и переместились во
//вспомогательный класс
{
public void WashCar(Car c)
{
Console.WriteLine("Cleaning a car");
else
Console.WriteLine("This car is already clean ...");
public void RotateTires(Car c)
{
Console.WriteLine("Tires have been rotated");
else
Console.WriteLine(" Dont need to be rotated ...");
public static int Main(string[] args)
{
Garage g = new Garage();
ServiceDept sd = new ServiceDept();
Car.CarDelegate wash = new Car.CarDelegate(sd.WashCar);
Car.CarDelegate rotate = new Car.CarDelegate(sd.RotateTires);
Обратите внимание на имя вызываемого метода.
Cleaning a car
Dont need to be rotated ...
This car is already clean ...
Dont need to be rotated ...
This car is already clean ...
Tires have been rotated
Cleaning a car
Tires have been rotated
This car is already clean ...
Tires have been rotated