Книга: C# 4.0: полное руководство
Делегаты
Разделы на этой странице:
Делегаты
Начнем с определения понятия делегата. Попросту говоря, делегат представляет собой объект, который может ссылаться на метод. Следовательно, когда создается делегат, то в итоге получается объект, содержащий ссылку на метод.
Более того, метод можно вызывать по этой ссылке. Иными словами, делегат позволяет вызывать метод, на который он ссылается. Ниже будет показано, насколько действенным оказывается такой принцип.
Следует особо подчеркнуть, что один и тот же делегат может быть использован для вызова разных методов во время выполнения программы, для чего достаточно изменить метод, на который ссылается делегат. Таким образом, метод, вызываемый делегатом, определяется во время выполнения, а не в процессе компиляции. В этом, собственно, и заключается главное преимущество делегата.
------------------------------------
ПРИМЕЧАНИЕ
Если у вас имеется опыт программирования на C/C++, то вам полезно будет знать, что делегат в C# подобен указателю на функцию в C/C++.
------------------------------------
Тип делегата объявляется с помощью ключевого слова delegate. Ниже приведена общая форма объявления делегата:
delegate возвращаемый_тип имя(список_параметров);
где возвращаемый_тип обозначает тип значения, возвращаемого методами, которые будут вызываться делегатом; имя — конкретное имя делегата; список_параметров — параметры, необходимые для методов, вызываемых делегатом. Как только будет создан экземпляр делегата, он может вызывать и ссылаться на те методы, возвращаемый тип и параметры которых соответствуют указанным в объявлении делегата.
Самое главное, что делегат может служить для вызова любого метода с соответствующей сигнатурой и возвращаемым типом. Более того, вызываемый метод может быть методом экземпляра, связанным с отдельным объектом, или же статическим методом, связанным с конкретным классом. Значение имеет лишь одно: возвращаемый тип и сигнатура метода должны быть согласованы с теми, которые указаны в объявлении делегата.
Для того чтобы показать делегат в действии, рассмотрим для начала простой пример его применения.
// Простой пример применения делегата.
using System;
// Объявить тип делегата,
delegate string StrMod(string str);
class DelegateTest {
// Заменить пробелы дефисами.
static string ReplaceSpaces(string s) {
Console.WriteLine("Замена пробелов дефисами.");
return s.Replace(' ', '-');
}
// Удалить пробелы.
static string RemoveSpaces(string s) {
string temp = "";
int i;
Console.WriteLine("Удаление пробелов.");
for(i=0; i < s.Length; i++)
if(s[i] != ' ') temp += s[i];
return temp;
}
// Обратить строку.
static string Reverse(string s) {
string temp = "";
int i, j;
Console.WriteLine("Обращение строки. ");
for(j=0, i=s.Length-1; i >= 0; i--, j++) temp += s[i];
return temp;
}
static void Main() {
// Сконструировать делегат.
StrMod strOp = new StrMod(ReplaceSpaces);
string str;
// Вызвать методы с помощью делегата,
str = strOp("Это простой тест.");
Console.WriteLine("Результирующая строка: " + str);
Console.WriteLine();
strOp = new StrMod(RemoveSpaces);
str = strOp("Это простой тест.");
Console.WriteLine("Результирующая строка: " + str);
Console.WriteLine();
strOp = new StrMod(Reverse);
str = strOp("Это простой тест.");
Console.WriteLine("Результирующая строка: " + str);
}
}
Вот к какому результату приводит выполнение этого кода.
Замена пробелов дефисами.
Результирующая строка: Это-простой-тест.
Удаление пробелов.
Результирующая строка: Этопростойтест.
Обращение строки.
Результирующая строка: .тсет йотсорп отЭ
Рассмотрим данный пример более подробно. В его коде сначала объявляется делегат StrMod
типа string
, как показано ниже.
delegate string StrMod(string str);
Как видите, делегат StrMod
принимает один параметр типа string
и возвращает одно значение того же типа.
Далее в классе DelegateTest
объявляются три статических метода с одним параметром типа string
и возвращаемым значением того же типа. Следовательно, они соответствуют делегату StrMod
. Эти методы видоизменяют строку в той или иной форме. Обратите внимание на то, что в методе Rep la се Spaces() для замены пробелов дефисами используется один из методов типа string — Replace()
.
В методе Main()
создается переменная экземпляра strOp
ссылочного типа StrMod
и затем ей присваивается ссылка на метод ReplaceSpaces()
. Обратите особое внимание на следующую строку кода.
StrMod strOp = new StrMod(ReplaceSpaces);
В этой строке метод ReplaceSpaces()
передается в качестве параметра. При этом указывается только его имя, но не параметры. Данный пример можно обобщить: при получении экземпляра делегата достаточно указать только имя метода, на который должен ссылаться делегат. Ясно, что сигнатура метода должна совпадать с той, что указана в объявлении делегата. В противном случае во время компиляции возникнет ошибка.
Далее метод ReplaceSpaces()
вызывается с помощью экземпляра делегата strOp
, как показано ниже.
str = strOp("Это простой тест.");
Экземпляр делегата strOp
ссылается на метод ReplaceSpaces()
, и поэтому вызывается именно этот метод.
Затем экземпляру делегата strOp
присваивается ссылка на метод RemoveSpaces()
, и с его помощью вновь вызывается указанный метод — на этот раз RemoveSpaces()
.
И наконец, экземпляру делегата strOp
присваивается ссылка на метод Reverse()
. А в итоге вызывается именно этот метод.
Главный вывод из данного примера заключается в следующем: в тот момент, когда происходит обращение к экземпляру делегата strOp
, вызывается метод, на который он ссылается. Следовательно, вызов метода разрешается во время выполнения, а не в процессе компиляции.
Групповое преобразование делегируемых методов
Еще в версии C# 2.0 было внедрено специальное средство, существенно упрощающее синтаксис присваивания метода делегату. Это так называемое групповое преобразование методов, позволяющее присвоить имя метода делегату, не прибегая к оператору new или явному вызову конструктора делегата.
Ниже приведен метод Main()
из предыдущего примера, измененный с целью продемонстрировать групповое преобразование методов.
static void Main() {
// Сконструировать делегат,
// используя групповое преобразование методов.
StrMod strOp = ReplaceSpaces; // использовать групповое преобразование методов
string str;
// Вызвать методы с помощью делегата,
str = strOp("Это простой тест.");
Console.WriteLine("Результирующая строка: " + str);
Console.WriteLine();
strOp = RemoveSpaces; // использовать групповое преобразование методов
str = strOp("Это простой тест.");
Console.WriteLine("Результирующая строка: " + str);
Console.WriteLine() ;
strOp = Reverse; // использовать групповое преобразование методов
str = strOp("Это простой тест.");
Console.WriteLine("Результирующая строка: " + str);
Console.WriteLine() ;
}
Обратите особое внимание на то, как создается экземпляр делегата strOp
и как ему присваивается метод ReplaсеSpaces
в следующей строке кода.
strOp = RemoveSpaces; // использовать групповое преобразование методов
В этой строке кода имя метода присваивается непосредственно экземпляру делегата strOp
, а все заботы по автоматическому преобразованию метода в тип делегата "возлагаются" на средства С#. Этот синтаксис может быть распространен на любую ситуацию, в которой метод присваивается или преобразуется в тип делегата.
Синтаксис группового преобразования методов существенно упрощен по сравнению с прежним подходом к делегированию, поэтому в остальной части книги используется именно он.
Применение методов экземпляра в качестве делегатов
В предыдущем примере использовались статические методы, но делегат может ссылаться и на методы экземпляра, хотя для этого требуется ссылка на объект. Так, ниже приведен измененный вариант предыдущего примера, в котором операции со строками инкапсулируются в классе StringOps
. Следует заметить, что в данном случае может быть также использован синтаксис группового преобразования методов.
// Делегаты могут ссылаться и на методы экземпляра.
using System;
// Объявить тип делегата,
delegate string StrMod(string str);
class StringOps {
// Заменить пробелы дефисами.
public string ReplaceSpaces(string s) {
Console.WriteLine("Замена пробелов дефисами.");
return s.Replace(' ', '-');
}
// Удалить пробелы.
public string RemoveSpaces(string s) {
string temp = "";
int i;
Console.WriteLine("Удаление пробелов.");
for(i=0; i < s.Length; i++)
if(s[i] != ' ') temp += s[i];
return temp;
}
// Обратить строку,
public string Reverse(string s) {
string temp = "";
int i, j;
Console.WriteLine("Обращение строки.");
for(j=0, i=s.Length-1; i >= 0; i--, j++) temp += s[i];
return temp;
}
}
class DelegateTest {
static void Main() {
StringOps so = new StringOps(); // создать экземпляр
// объекта класса StringOps
// Инициализировать делегат.
StrMod strOp = so.ReplaceSpaces;
string str;
// Вызвать методы с помощью делегатов,
str = strOp("Это простой тест.");
Console.WriteLine("Результирующая строка: " + str);
Console.WriteLine();
strOp = so.RemoveSpaces;
str = strOp("Это простой тест.");
Console.WriteLine("Результирующая строка: " + str);
Console.WriteLine();
strOp = so.Reverse;
str = strOp("Это простой тест.");
Console.WriteLine("Результирующая строка: " + str);
}
}
Результат выполнения этого кода получается таким же, как и в предыдущем примере, но на этот раз делегат обращается к методам по ссылке на экземпляр объекта класса StringOps
.
Групповая адресация
Одним из самых примечательных свойств делегата является поддержка групповой адресации. Попросту говоря, групповая адресация — это возможность создать список, или цепочку вызовов, для методов, которые вызываются автоматически при обращении к делегату. Создать такую цепочку нетрудно. Для этого достаточно получить экземпляр делегата, а затем добавить методы в цепочку с помощью оператора + или +=. Для удаления метода из цепочки служит оператор - или -=. Если делегат возвращает значение, то им становится значение, возвращаемое последним методом в списке вызовов. Поэтому делегат, в котором используется групповая адресация, обычно имеет возвращаемый тип void.
Ниже приведен пример групповой адресации. Это переработанный вариант предыдущих примеров, в котором тип значений, возвращаемых методами манипулирования строками, изменен на void, а для возврата измененной строки в вызывающую часть кода служит параметр типа ref. Благодаря этому методы оказываются более приспособленными для групповой адресации.
// Продемонстрировать групповую адресацию.
using System;
// Объявить тип делегата.
delegate void StrMod(ref string str);
class MultiCastDemo {
// Заменить пробелы дефисами.
static void ReplaceSpaces(ref string s) {
Console.WriteLine("Замена пробелов дефисами.");
s = s.Replace(' ' , '-');
}
// Удалить пробелы.
static void RemoveSpaces(ref string s) {
string temp = "";
int i;
Console.WriteLine("Удаление пробелов.");
for(i=0; i < s.Length; i++)
if(s[i] != ' ') temp += s[i];
s = temp;
}
// Обратить строку.
static void Reverse(ref string s) {
string temp = "";
int i, j;
Console.WriteLine("Обращение строки.");
for(j=0, i=s.Length-1; i >= 0; i—, j++) temp += s[i];
s = temp;
}
static void Main() {
// Сконструировать делегаты.
StrMod strOp;
StrMod replaceSp = ReplaceSpaces;
StrMod removeSp = RemoveSpaces;
StrMod reverseStr = Reverse;
string str = "Это простой тест.";
// Организовать групповую адресацию.
strOp = replaceSp;
strOp += reverseStr;
// Обратиться к делегату с групповой адресацией.
strOp(ref str);
Console.WriteLine("Результирующая строка: " + str);
Console.WriteLine();
// Удалить метод замены пробелов и добавить метод удаления пробелов.
strOp -= replaceSp;
strOp += removeSp;
str = "Это простой тест."; // восстановить исходную строку
// Обратиться к делегату с групповой адресацией.
strOp(ref str);
Console.WriteLine("Результирующая строка: " + str);
Console.WriteLine() ;
}
}
Выполнение этого кода приводит к следующему результату.
Замена пробелов дефисами.
Обращение строки.
Результирующая строка: .тсет-йотсорп-отЭ
Обращение строки.
Удаление пробелов.
Результирующая строка: .тсетйотсорпотЭ
В методе Main()
из рассматриваемого здесь примера кода создаются четыре экземпляра делегата. Первый из них, strOp
, является пустым, а три остальных ссылаются на конкретные методы видоизменения строки. Затем организуется групповая адресация для вызова методов RemoveSpaces()
и Reverse()
. Это делается в приведенных ниже строках кода.
strOp = replaceSp;
strOp += reverseStr
Сначала делегату strOp
присваивается ссылка replaceSp
, а затем с помощью оператора +=
добавляется ссылка reverseStr
. При обращении к делегату strOp
вызываются оба метода, заменяя пробелы дефисами и обращая строку, как и показывает приведенный выше результат.
Далее ссылка replaceSp
удаляется из цепочки вызовов в следующей строке кода:
strOp -= replaceSp;
и добавляется ссылка removeSp
в строке кода.
strOp += removeSp;
После этого вновь происходит обращение к делегату strOp
. На этот раз обращается строка с удаленными пробелами.
Цепочки вызовов являются весьма эффективным механизмом, поскольку они позволяют определить ряд методов, выполняемых единым блоком. Благодаря этому улучшается структура некоторых видов кода. Кроме того, цепочки вызовов имеют особое значение для обработки событий, как станет ясно в дальнейшем.
Ковариантность и контравариантность
Делегаты становятся еще более гибкими средствами программирования благодаря двум свойствам: ковариантности и контравариантности. Как правило, метод, передаваемый делегату, должен иметь такой же возвращаемый тип и сигнатуру, как и делегат. Но в отношении производных типов это правило оказывается не таким строгим благодаря ковариантности и контравариантности. В частности, ковариантность позволяет присвоить делегату метод, возвращаемым типом которого служит класс, производный от класса, указываемого в возвращаемом типе делегата. А контравариантность позволяет присвоить делегату метод, типом параметра которого служит класс, являющийся базовым для класса, указываемого в объявлении делегата.
Ниже приведен пример, демонстрирующий ковариантность и контравариантность.
// Продемонстрировать ковариантность и контравариантность.
using System;
class X {
public int Val;
}
// Класс Y, производный от класса X.
class Y : X { }
// Этот делегат возвращает объект класса X и
// принимает объект класса Y в качестве аргумента,
delegate X ChangeIt(Y obj);
class CoContraVariance {
// Этот метод возвращает объект класса X и
// имеет объект класса X в качестве параметра,
static X IncrA(X obj) {
X temp = new X();
temp.Val = obj.Val + 1;
return temp;
}
// Этот метод возвращает объект класса Y и
// имеет объект класса Y в качестве параметра,
static Y IncrB(Y obj) {
Y temp = new Y();
temp.Val = obj.Val + 1;
return temp;
}
static void Main() {
Y Yob = new Y();
// В данном случае параметром метода IncrA является объект класса X,
// а параметром делегата Changelt — объект класса Y. Но благодаря
// контравариантности следующая строка кода вполне допустима.
ChangeIt change = IncrA;
X Xob = change(Yob);
Console.WriteLine("Xob: " + Xob.Val);
// В этом случае возвращаемым типом метода IncrB служит объект класса Y,
// а возвращаемым типом делегата Changelt — объект класса X.
// Но благодаря ковариантности следующая строка кода
// оказывается вполне допустимой,
change = IncrB;
Yob = (Y) change(Yob);
Console.WriteLine("Yob: " + Yob.Val);
}
}
Вот к какому результату приводит выполнение этого кода.
Xob: 1
Yob: 1
В данном примере класс Y является производным от класса X. А делегат ChangeIt
объявляется следующим образом.
delegate X ChangeIt(Y obj);
Делегат возвращает объект класса X и принимает в качестве параметра объект класса Y. А методы IncrA()
и IncrB()
объявляются следующим образом.
static X IncrA(X obj)
static Y IncrB(Y obj)
Метод IncrA()
принимает объект класса X в качестве параметра и возвращает объект того же класса. А метод IncrB()
принимает в качестве параметра объект класса Y и возвращает объект того же класса. Но благодаря ковариантности и контравари-антности любой из этих методов может быть передан делегату ChangeIt
, что и демонстрирует рассматриваемый здесь пример.
Таким образом, в строке
ChangeIt change = IncrA;
метод IncrA()
может быть передан делегату благодаря контравариантности, так как объект класса X служит в качестве параметра метода IncrA()
, а объект класса Y — в качестве параметра делегата ChangeIt
. Но метод и делегат оказываются совместимыми в силу контравариантности, поскольку типом параметра метода, передаваемого делегату, служит класс, являющийся базовым для класса, указываемого в качестве типа параметра делегата.
Приведенная ниже строка кода также является вполне допустимой, но на этот раз благодаря ковариантности.
change = IncrB;
В данном случае возвращаемым типом для метода IncrB()
служит класс Y, а для делегата — класс X. Но поскольку возвращаемый тип метода является производным классом от возвращаемого типа делегата, то оба оказываются совместимыми в силу ковариантности.
Класс System. Delegate
Все делегаты и классы оказываются производными неявным образом от класса System.Delegate
. Как правило, членами этого класса не пользуются непосредственно, и это Не делается явным образом в данной книге. Но члены класса System.Delegate
могут оказаться полезными в ряде особых случаев.
Назначение делегатов
В предыдущих примерах был наглядно продемонстрирован внутренний механизм действия делегатов, но эти примеры не показывают их истинное назначение. Как правило, делегаты применяются по двум причинам. Во-первых, как упоминалось ранее в этой главе, делегаты поддерживают события. И во-вторых, делегаты позволяют вызывать методы во время выполнения программы, не зная о них ничего определенного в ходе компиляции. Это очень удобно для создания базовой конструкции, допускающей подключение отдельных программных компонентов. Рассмотрим в качестве примера графическую программу, аналогичную стандартной сервисной программе Windows Paint. С помощью делегата можно предоставить пользователю возможность подключать специальные цветные фильтры или анализаторы изображений. Кроме того, пользователь может составлять из этих фильтров или анализаторов целые последовательности. Подобные возможности программы нетрудно обеспечить, используя делегаты.