Одно из полезных нововведений в С# — делегаты (delegates). Их назначение по сути совпадает с указателями функций в C++, но делегаты являются управляемыми объектами и привязаны к типам. Это значит, что исполняющая среда (runtime) гарантирует, что делегат указывает на допустимый объект, а это в свою очередь означает получение всех достоинств указателей функций без связанных с этим опасностей, таких как применение недопустимых адресов или разрушение памяти других объектов. В этой главе мы рассмотрим делегаты в сравнении с интерфейсами, их синтаксис и проблемы их применения. Мы также увидим несколько примеров использования делегатов с функциями обратного вызова и асинхронными обработчиками событий.
Из главы 9 мы узнали как определяются и реализуются интерфейсы. Как вы знаете, с концептуальной точки зрения интерфейсы — это связки между двумя различными частями кода. Но при этом интерфейсы во многом напоминают классы, так как объявляются в период компиляции и могут включать методы, свойства, индексаторы и события. Что касается делегата, то он ссылается на единственный метод и определяется в период выполнения. В С# две основных области применения делегатов: методы обратного вызова и обработчики событий.
Использование делегатов в качестве методов обратного вызова
Методы обратного вызова повсеместно используются в Windows для передачи указателя функции другой функции, чтобы последняя могла вызвать первую (через переданный ей указатель). Так, функция Win32 API
EnumWindows перечисляет все окна верхнего уровня на экране и для каждого окна вызывает переданную ей функцию. Обратные вызовы применяют по-разному, но наиболее распространены два случая.
Рассмотрим пример определения и применения делегата. В этом примере у нас есть класс менеджера базы данных, который отслеживает все активные соединения с БД и предоставляет метод перечисления этих соединений. Если допустить, что менеджер БД находится на удаленной машине, правильным решением будет сделать метод асинхронным, позволив клиенту предоставлять метод обратного вызова. Заметьте: в реальном приложении вам следовало бы задействовать многопоточность, чтобы добиться подлинной асинхронности. Но для упрощения примера и поскольку мы не рассматривали многопоточность, пока не будем ее применять.
Для начала определим два главных класса: DBManager n DBConnection.
class DBConnection <
}
class DBManager { static DBConnection[] activeConnections;
public delegate void EnumConnectionsCallback(DBConnection
connection); public static void EnumConnections(EnumConnectionsCallback
callback) {
foreach (DBConnection connection in activeConnections)
{
callback(connection);
} > }
Метод EnumConnectionsCallback является делегатом, что определяется ключевым словом delegate в начале сигнатуры метода. Как видите, этот делегат возвращает void и принимает единственный аргумент — объект DBConnection. Метод EnumConnections в соответствии с его определением принимает единственный аргумент — метод EnumConnectionsCallback. Чтобы вызвать метод DBManager.EnumConnections нам нужно лишь передать ему экземпляр делегата DBManager.EnumConnectionCallback.
Для создания экземпляра делегата нужно применить new, передав ему имя метода, имеющего ту же сигнатуру, что и у делегата. Вот пример:
DBManager.EnumConnectionsCallback myCallback =
new DBManager.EnumConnectionsCallback(ActiveConnectionsCallback);
DjlManager.EnumConnections(myCallback);
Заметьте, что это можно скомбинировать в единый вызов:
DBManager.EnumConnections(new
DBManager.EnumConnectionsCallback(ActiveConnectionsCallback));
Вот и все, что касается базового синтаксиса делегатов. Теперь посмотрим на законченный пример:
using System;
class DBConnection {
public DBConnection(string name) {
this.name = name; }
protected string Name; public string name {
get {
return this.Маше; }
set {
this.Name = value; > } }
class DBManager {
static DBConnection[] activeConnections;
public void AddConnectionsO
{
activeConnections = new DBConnection[5];
for (int i = 0; i < 5; i++)
{
activeConnections[i] = new DBConnection("DBConnection " + (i + 1)); > }
public delegate void EnumConnectionsCallback(DBConnection
connection); public static void EnumConnections(EnumConnectionsCallback
callback) {
foreach (DBConnection connection in activeConnections)
{
callback(connection);
} } >
class DelegatelApp {
public static void ActiveConnectionsCallback(DBConnection
connection) <
Console.WriteLine("Callback method called for "
+ connection.name); }
public static void Main() {
DBManager dbMgr = new DBManagerO; dbMg r.AddConnections();
DBManager.EnumConnectionsCallback myCallback = new DBManager.EnumConnectionsCallback (ActiveConnectionsCallback);
DBManager.EnumConnections(myCallback); } }
После компиляции и запуска этого приложения мы получим такие результаты:
Callback method called for DBConnection 1 Callback method called for DBConnection 2 Callback method called for DBConnection 3 Callback method called for DBConnection 4 Callback method called for DBConnection 5
Определение делегатов в качестве статических членов
Довольно неуклюжее решение создавать экземпляр делегата при каждом его применении, но в С# можно определять метод, который используется при создании делегата как статический член класса. Ниже приведен предыдущий пример с применением такого подхода. Заметьте, что теперь делегат определен как статический член класса myCallback и этот член может использоваться в методе Main, так что клиенту нет нужды создавать экземпляр делегата.
using System;
class DBConnection {
public DBConnection(string name)
<
this.name = name;
>
protected string Name; public string name {
get {
return this.Name; >
set {
this.Name = value; } } >
class DBManager {
static DBConnection[] activeConnections;
public void AddConnectionsO
{
activeConnections = new DBConnection[5];
for (int i = 0; i < 5; i++)
{
activeConnections[i] = new DBConnection("DBConnection " + (i + 1))-} }
public delegate void EnumConnectionsCallback(DBConnection connection);
public static void EnumConnections(EnumConnectionsCallback
callback) {
foreach (DBConnection connection in activeConnections)
callback(connection); } } }
class Delegate2App {
public static DBManager.EnumConnectionsCallback myCallback = new DBManager.EnumConnectionsCallback (ActiveConnectionsCallback);
public static void ActiveConnectionsCallback(DBConnection
connection) {
Console.WriteLine ("Callback method called for " +
connection.name); }
public static void MainQ {
DBManager dbMgr = new DBManagerQ;
dbMgr. AddConnectionsO;
DBManager. EnurnConnections(myCallback); } >
ПРИМЕЧАНИЕ Поскольку общим правилом именования
делегатов является добавление слова Callback к имени метода, принимающего
делегат в качестве аргумента, можно по ошибке использовать имя этого метода
вместо имени делегата. При этом компилятор уведомит, что вы указали метод там,
где ожидается класс. Получив такую ошибку, помните: проблема в том, что вы указали
метод вместо делегата.
Создавайте делегаты только при необходимости
В двух примерах, которые мы рассмотрели, делегаты создаются независимо от
того, будут ли они когда-либо использоваться. В рассмотренных примерах ничего
плохого в этом нет, поскольку известно, что они будут вызываться. Но в целом
при определении делегатов важно решить, когда их создавать. Скажем, может случиться,
что создание делегатов потребует немалого времени и затрат. В таких случаях,
когда вы знаете, что клиент обычно не обращается к данному методу обратного
вызова, можно отложить создание делегата до момента, когда он действительно
потребуется, заключив создание его экземпляра в оболочку свойства. Это иллюстрирует
следующий измененный класс DBManager, в котором для создания экземпляра
делегата используется неизменяемое свойство (поскольку для него представлен
только метод-получатель), using System;
class DBConnection {
public DBConnection(string name)
{
this.name = name;
}
protected string Name; public string name {
get {
return this.Name; }
set {
this.Name = value; } } }
class DBManager {
static DBConnection[] activeConnections;
public void AddConnections()
{
activeConnections = new DBConnection[5];
for (int i = 0; i < 5; i++)
{
activeConnections[i] = new
DBConnection("DBConnection " + (i + 1)); }
}
public delegate void EnumConnectionsCallback(DBConnection
connection); public static void EnumConnections(EnumConnectionsCallback
callback) {
foreach (DBConnection connection in activeConnections)
{
callback(connection); } } }
class DelegateSApp <
public DBManager.EnumConnectionsCallback myCallback
{
get
{
return new DBManager.EnumConnectionsCallback (ActiveConnectionsCallback); } >
public static void ActiveConnectionsCallback(DBConnection
connection) {
Console.WriteLine
("Callback method called for " + connection.name); }
Bublic static void MainQ {
DelegateSApp app = new Delegate3App();
DBManager dbMgr = new DBManagerO; dbMgr.AddConnections();
DBManager.EnumConnections(app. myCallback); }
>
Объединение делегатов — создание одного делегата из нескольких — одна из тех возможностей, которая поначалу не кажется такой уж полезной, но если вы столкнетесь с такой потребностью, то будете признательны команде разработчиков С# за то, что они это предусмотрели. Разберем некоторые примеры, когда может быть полезно объединение делегатов. В первом примере мы имеем дистрибьюторскую систему и класс, просматривающий все наименования товара на данном складе, вызывая метод обратного вызова для каждого наименования, запасов которого менее 50 единиц. В реальном дистрибьюторском приложении формула должна учитывать не только наличные запасы, но также заказанные и находящиеся "в пути". Но возьмем простой пример: если наличие на складе менее 50 единиц, возникает исключение.
Фокус в том, что мы хотим разделить методы, которые вызываются, если запасы ниже допустимых: нам нужно запротоколировать сам факт и, кроме того, послать письмо по электронной почте менеджеру по закупкам. Итак, составим делегат из нескольких других:
using System;
using System.Threading;
class Part {
public Part(string sku)
{
this.Sku = sku;
Random r = new Random(DateTime.Now.Millisecond); double d = r.NextDoubleO * 100;
this.OnHand = (int)d; }
protected string Sku; public string sku {
get
{
return this.Sku;
}
set
{
this.Sku = value;
} }
protected int OnHand; public int onhand
{
get
{
return this.OnHand;
}
set
{
this.OnHand = value;
} } }
class InventoryManager
{
protected const int MIN_ONHAND = 50;
public Part[] parts; public InventoryManagerO
{
parts = new Part[5];
for (int 1=0; i < 5; i++)
{
Part part = new Part("Part " + (i + 1));
> Thread.Sleep(10); // Генератор случайных чисел,
// установленный по времени.
partsti] = part;
Console,WriteLine("Adding part '{0}' on-hand = {1}", part.sku, part.onhand);
} }
public delegate void OutOfStockExceptionMethod(Part part); public void ProcessInventory(OutOfStockExceptionMethod exception)
{
Console.WriteLine("\nProcessing inventory...");
foreach (Part part in parts)
{
if (part.onhand < MIN_ONHAND) {
Console.WriteLine
("{0} ({1}) is below minimum on-hand {2}", part.sku, part.onhand, MIN_ONHAND);
exception(part); > } } }
class ComposlteDelegatelApp {
public static void LogEvent(Part part)
{
Console.WriteLine("\tlogging event...");
}
public static void EmailPurchasingMgr(Part part) {
Console.WriteLine("\teraailing Purchasing manager..."); }
public static void MainQ {
InventoryManager mgr = new InventoryManagerO;
InventoryManager.OutOfStockExceptionMethod LogEventCallback = new InventoryManager.OutOfStockExceptionMethod(LogEvent);
Invento ryManage r.OutOfStockExceptionMethod EmailPurchasingNgrCallback = new InventoryManager.OutOfStockExceptionMethod (EmailPurchasingMgr);
InventoryManager.OutOfStockExceptionMethod OnHandExceptionEventsCallback =
EmailPurchasingMgrCallback + LogEventCallback;
mgr.ProcessInventory(OnHandExceptionEventsCallback); } }
В результате выполнения мы увидим результат вроде этого:
Adding part 'Part V on-hand = 16
Adding part 'Part 2' on-hand = 98
Adding part 'Part 3' on-hand = 65
Adding part 'Part 4' on-hand = 22
Adding part 'Part 5' on-hand = 70
Processing inventory...
Part 1 (16) is below minimum on-hand 50
logging event...
entailing Purchasing manager... Part 4 (22) is below minimum on-hand 50
logging event...
emailing Purchasing manager...
Таким образом, эта возможность языка позволяет динамически определять, какие методы нужно включать в метод обратного вызова, и объединять их в составной делегат. Исполняющая среда распознает, что эти методы нужно вызвать последовательно. Кроме того, вы можете убрать любой делегат из составного оператором "минус".
Тот факт, что эти методы вызываются последовательно, заставляет спросить: почему просто не связать методы в цепочку, чтобы каждый метод вызывал последующий? В нашем примере, где у нас всего два метода и вызываются они всегда оба, это сделать можно. Но усложним при-- мер. Допустим, у нас несколько магазинов, расположенных в разных местах, и каждый сам решает какие методы вызывать. К примеру, на территории одного магазина находится общий товарный склад, и здесь нужно запротоколировать событие и сообщить менеджеру о закупках, а в других магазинах — запротоколировать событие и сообщить управляющему магазина.
Такое требование легко реализовать, динамически создавая составной делегат на основе информации о конкретном местоположении магазина. Без делегатов нам пришлось бы написать метод, который не только определял бы, какие методы вызывать, но и отслеживал, какие методы уже вызывались, а какие еще нужно вызвать. Смотрите, как делегаты упрощают эту потенциально сложную операцию.
using System;
class Part {
public Part(string sku)
{
this.Sku = sku;
Random г = new Random(DateTime.Now.Millisecond); double d = r.NextDoubleO * 100;
this.OnHand = (int)d; }
protected string Sku; public string sku {
get
{
return this.Sku;
}
set
{
this.Sku = value;
} }
protected int OnHand; public int onhand {
get {
return this.OnHand; }
set {
this.OnHand = value; } } }
class InventoryManager {
protected const int MIN_ONHAND = 50;
public Part[] parts; public InventoryManagerO {
parts = new Part[5];
for (int i = 0; i < 5; i++)
{
Part part = new Part("Part " + (i + 1)); parts[i] = part; Console.WriteLine
("Adding part '{0}' on-hand = {1}", part.sku, part.onhand); } }
public delegate void OutOfStockExceptionMethod(Part part); public void ProcessInventory(OutOfStockExceptionMethod exception) {
Console.WriteLine("\nProcessing inventory...");
foreach (Part part in parts)
<
if (part.onhand < MIN_ONHAND) {
Console.WriteLine
("{0} ({1" is below minimum onhand {2}", part.sku, part.onhand, MIN_ONHANO);
exception(part); } } } }
class CompositeDelegate2App {
public static void LogEvent(Part part)
{
Console.WriteLine("\tlogging event...");
>
public static void EmailPurchasingMgr(Part part)
{
Console.WriteLine("\temailing Purchasing manager..,"); }
public static void EmailStoreMgr(Part part) <
Console.WriteLine("\temailing store manager..."); >
public static void MainQ {
InventoryManager mgr = new InventoryManagerO;
InventoryManager.OutOfStockExceptionMethod[]
exceptionMethods = new
InventoryManager.OutOfStockExceptionMethod[3]; exceptionMethods[0] = new
InventoryManager.OutOfStockExceptionMethod
(LogEvent); exceptionMethods[1] = new
InventoryManager.OutOfStockExceptionMethod
(EmailPurchasingMgr); exceptionMethods[2] = new
InventoryManager.OutOfStockExceptionMethod (EmailStoreMgr);
int location = 1;
InventoryManager.OutOfStockExceptionMethod compositeDelegate;
if (location == 2) {
compositeDelegate =
exceptionMethods[0] + exceptionMethods[1]; }
else {
compositeDelegate =
exceptionMethods[0] + exceptionMethods[2]; }
mgr.ProcessInventory(compositeDelegate); } }
Теперь при компиляции и выполнении этого приложения результаты будут отличаться в зависимости от значения переменной location.
Определение событий с помощью делегатов
Практически во всех Windows-приложениях требуется асинхронная обработка событий. Некоторые из этих событий связаны с самой ОС, например, когда Windows посылает сообщения в очередь сообщений при-\ ложения при том или ином взаимодействии пользователя с приложени-I ем. Другие больше связаны с предметной областью, например, когда \ нужно распечатать счет при обновлении заказа.
, Работа с событиями в С# соответствует модели "издатель — подписчик", где класс публикует событие, которое он может инициировать, и любые классы могут подписаться на это событие. При инициации события исполняющая среда следит за тем, чтобы уведомить всех подписчиков о возникновении события.
Метод, вызываемый при возникновении события, определяется делегатом. Однако нужно помнить об ограничениях, которые налагаются на делегаты, используемые для этих целей. Во-первых, нужно чтобы такой делегат принимал два аргумента. Во-вторых, эти аргументы всегда должны представлять два объекта: тот, что инициировал событие (издатель), и информационный объект события, который должен быть производным от класса EventArgs .NET Framework.
Скажем, мы хотим отслеживать изменения объемов запасов. Мы создаем класс InventoryManager, который будет всегда использоваться при обновлении запасов. Этот класс должен публиковать события, возника-, ющие при всяком изменении запасов вследствие закупок, продаж и других причин. Тогда любой класс, которому нужно отслеживать изменения, должен подписаться на эти события. Вот как это делается на С# при помощи делегатов и событий:
using System;
class InventoryChangeEventArgs : EventArgs
{
public InventoryChange£ventArgs(string sku, int change)
{
this.sku = sku;
this.change = change; >
string sku; public string Sku {
get
{
return sku;
} }
int change; public int Change {
get {
return change; } } >
class InventoryManager // Издатель. {
public delegate void InventoryChangeEventHandler (object source, InventoryChangeEventArgs e); public event InventoryChangeEventHandler OnlnventoryChangeHandler;
public void Updatelnventory(string sku, int change) {
if (0 == change)
return; // Ничего обновлять не нужно.
// Здесь должен быть код для обновления БД.
InventoryChangeEventArgs e = new
InventoryChangeEventArgs(sku, change);
if (OnlnventoryChangeHandler != null) OnInventoryChangeHandler(this, e); } }
class InventoryWatcher // Подписчик.
{
public InventoryWatcher(InventoryManager inventoryManager) {
this.inventoryManager = inventoryManager;
inventoryManager.OnlnventoryChangeHandler += new
InventoryManager.InventoryChangeEventHandler
(Onlnvento ryChange); } void OnInventoryChange(object source, InventoryChangeEventArgs e)
I <
\ int change = e.Change;
\ Console.WriteLineC'Part '{0}' was {1} by {2} units", \ e.Sku,
change > 0 ? "increased" ; "decreased",
Math.Abs(e.Change));
}
InventoryManager inventoryManager;
}
class EventslApp {
public static void MainQ <
InventoryManager inventoryManager = new InventoryManagerO;
InventoryWatcher inventoryWatch =
new InventoryWatcher(inventoryManager);
inventoryManager.Updatelnventory("111 006 116", -2); inventoryManager.Updatelnventory("111 005 383", 5); > >
Рассмотрим первые два члена класса InventoryManager:
public delegate void InventoryChangeEventHandler
(object source, InventoryChangeEventArgs e);
public event InventoryChangeEventHandler OnlnventoryChangeHandler;
Первая строка кода — это делегат, вы можете узнать его по определению сигнатуры
метода. Как я говорил, все делегаты, используемые с событиями, должны быть определены
с двумя аргументами: объект-издатель (в данном случае source) и информационный
объект события (производный от EventArgs объект). Во второй строке указано
ключевое слово event, член, имеющий тип указанного делегата, и метод
(или методы), которые будут вызываться при возникновении события.
Update Inventory, — последний метод класса Inventory Manager — вызывается при каждом изменении запасов. Как видите, он создает объект типа InventoryChangeEventArgs, который передается всем подписчикам и описывает возникшее событие.
Теперь рассмотрим еще две строки кода:
if (OnlnventoryChangeHandler != null)
OnInventoryChangeHandler(this, e); I
Условный оператор // проверяет, есть ли подписчики, связанные с / методом OnlnventoryChangeHandler. Если это так, т. e. OnlnventoryChangeHandler не равен null, инициируется событие. Вот и все, что касается издателя. Теперь рассмотрим код подписчика.
Подписчик в нашем случае представлен классом Inventory Watcher. Ему нужно выполнять две простые задачи. Во-первых, он должен указать себя как подписчик, создав новый экземпляр делегата типа Inventory-Manager. InventoryChange Event Handler и добавив этот делегат к событию InventoryManager.OnlnventoryChangeHandler. Обратите особое внимание на синтаксис: чтобы добавить себя к списку подписчиков, он использует составной оператор присваивания +=, чтобы не уничтожить предыдущие подписчики.
inventoryManager.OnlnventoryChangeHandler +=
new InventoryManager.InventoryChangeEventHandler (OnlnventoryChange);
Единственный аргумент, указываемый здесь, — имя метода, вызываемого при возникновении события.
Последняя задача, которую должен выполнить подписчик, — это реализовать свой обработчик событий. В данном случае обработчик ошибок — InventoryWatcher.OnlnventoryChange — выводит сообщение об изменении запасов с указанием номера позиции.
И, наконец, в коде этого приложения при каждом вызове метода InventoryManager. Update Inventory создаются экземпляры классов Inventory-Managerи Inventory Watcher т автоматически инициируется событие, которое приводит к вызову метода InventoryWatcher.OnlnventoryChanged.
Подведем итоги
Делегаты — привязанные к типу управляемые объекты — играют в С# ту же роль, что и указатели функций в C++. Отличие делегатов от классов и интерфейсов в том, что они ссылаются на один метод и определяются в период выполнения. Делегаты широко применяются для асинхронной обработки и добавления нестандартного кода к коду классов. Делегаты могут использоваться для многих целей, включая методы обратного вызова, определение статических методов и обработку событий.