Делегаты в C#
Обычно во время разработки приложения выполняют вызовы подпрограмм, в которые передают требуемые параметры. В списке параметров может быть ссылка на метод.
Пример из языка С++. Объявление указателя на функцию:
void exp (int x, int y, double (*ptf) (double a[], int n));
Пример вызова
exp (5, y, prog (ar, 20));
При вызове подпрограммы, содержащей указатель на функцию, возможны варианты с возвратом значения и без возврата.
В этом случае вызываемая подпрограмма обращается к источнику вызова, запрашивая запуск указанного метода или выполняя так называемый «обратный вызов».
Этот прием широко применяется при разработке приложений с графическим интерфейсом, в которых программы, обеспечивающие функционирование элементов управления (меню, кнопок, списков и т. д.) вынуждены вызывать внешние подпрограммы, проверяющие случившееся событие и обеспечивающие требуемую реакцию.
Концепция «делегатов», появившаяся в языке C#, обеспечивает реакцию на «обратные вызовы» от объектов в приложении, но в отличие от указателей на функцию, имеющуюся в языке С++, делегаты представляют собой классы, обеспечивающие слежение за типами данных, имеющие встроенную поддержку группового выполнения и асинхронного вызова методов.
Делегатом является объект, содержащий ссылку на метод или на список методов. Один и тот же делегат может содержать ссылки на разные методы, имеющие одинаковый список аргументов (сигнатуру делегата). Выбор метода, на который ссылается делегат, может быть изменен в процессе выполнения программы.
С делегатами связаны два объявления:
• Объявление собственно делегата (тип делегата). Делегат является классом, поэтому объявление делегата - это создание объекта типа делегата. Конструктор при этом объявлении содержит список параметров для методов, которые будут передаваться делегату. Все передаваемые конкретному делегату методы имеют одинаковый список параметров в смысле количества параметров, их типов и порядка следования типов, а также тип возвращаемого значения.
• Объявление переменной типа делегата (экземпляр делегата). При этом объявлении также вызывается конструктор, но он содержит единственный параметр – имя метода, передаваемого делегату. Каждый экземпляр делегата обращается к одному и тому же методу. Существует так называемое «групповое» преобразование, при котором вместо объявления переменной типа делегата просто выполняется присваивание. Фактически это упрощенное объявление переменной типа делегата. Возможно, именно эта упрощенная форма приводит к нечетким формулировкам при объявлении делегатов.
Конструктор для переменной объявленного типа делегата учитывает следующую информацию:
• Ссылку на вызывающий метод.
• Список параметров вызываемого метода.
• Тип возвращаемого методом значения.
С обычными методами связано понятие «сигнатуры». Сигнатура это имя метода и список параметров с точки зрения типов (имена параметров не учитываются). При объявлении объекта типа делегата указывается также тип возвращаемого значения. Это отличает сигнатуру метода от сигнатуры делегата.
При объявлении делегата применяется ключевое слово delegate. Общая форма объявления объекта класса делегата имеет вид:
delegate тип_возврата имя_делегата (список_параметров);
где
тип_возврата указывает на тип значения для возврата методами, вызываемые делегатом.
список параметров - параметры, передаваемые методам, которые будут вызываться делегатом.
Ниже приведен пример объявления объекта типа делегата:
В качестве параметров для методов можно использовать имена подпрограмм, то есть ссылаться на методы (в методе для делегата имеется ссылка на еще один метод). Это обеспечивает возможность применения функций обратного вызова. Например, алгоритму сортировки можно передать ссылку на метод сравнения двух значений. Изменяя алгоритм сравнения в подпрограмме, можно изменять порядок сортировки.
Ниже приведен пример объявления делегата и его применения. Делегат объявляется два раза: сначала объявляется переменная типа делегата, а затем объект типа этого делегата (экземпляр). Объявленный делегат принимает методы с возвращаемым значением типа double, и единственным параметром является одномерный массив чисел того же типа. Делегат объявлен перед классом, в котором реализованы два статических метода (вычисление суммы и произведения элементов массива) и основная программа.
Обратите внимание:
1. Сигнатура делегата содержит тип возврата, имя делегата, список параметров.
2. Переменная типа объявленного делегата (экземпляр) объявлена один раз с именем ap. При объявлении в него был передан метод (Calc_Sum), а параметр (массив m) передается уже в имя (ap) экземпляра делегата ArProc (это похоже на вызов конструктора с параметрами).
3. Для передачи делегату другого метода используется тот же делегат, но справа от знака равенства он как бы создается заново, и ему передается имя другого метода.
class Program
{
delegate double ArProc(double [ ] ar);
class DelegateTest
{
// Подсчитать сумму элементов массива
static double Calc_Sum (double [ ] a)
{
double sum = 0;
Console.WriteLine("Подсчитать сумму элементов массива");
foreach (double e in a)
sum += e;
return sum;
}
// Подсчитать произведение элементов массива
static double Calc_Prod (double[ ] a)
{
double pr = 1;
Console.WriteLine("Подсчитать произведение элементов массива");
foreach (double e in a)
pr *= e;
return pr;
}
static void Main()
{
double [ ] m = {1.5, 2.5, 3};// Объявление и инициализация массива
// Объявление переменной типа делегата
ArProc ap = new ArProc (Calc_Sum);
// Вызвать с помощью делегата метод для суммирования.
// Переменная res принимает возврат от метода Calc_Sum
double res = ap(m); // ap уже объявлено, передать параметры
Console.WriteLine ("Вычисленная сумма = {0:F2} ", res);
// Передать делегату другой метод
ap = new ArProc (Calc_Prod);
res = ap(m);
Console.WriteLine ("Вычисленное произведение = {0:F2} ", res);
Console.ReadKey();
}
}
}
Связь между делегатом и классом, в котором определены методы, задается порядком следования класса с методами сразу за объявлением делегата. При изменении порядка размещения выдается сообщение об отсутствии указанных методов.
Ниже приведен результат выполнения программы.
Связь делегата с методом можно упростить посредством так называемого группового преобразования методов, при котором опускается операция new, и явно указывается имя метода. Форма объявления переменной типа делегата (экземпляра) получает вид:
ArProc ap = Calc_Sum;
Следует подчеркнуть, что объекту типа делегата (экземпляру) должна присваиваться ссылка на метод, а не возвращаемое методом значение.
Для возврата значения необходимо после имени объявленного делегата записать круглые скобки (пустые или со списком параметров).
В приведенном примере класс DelegateTest объявлен без модификатора, а это значит, что он имеет уровень доступа private. Такой же уровень доступа имеют и объявленные в этом классе методы.
Если класс с методами размещен не сразу после объявления делегата и требуется обеспечить правильную обработку и в этом случае, то надо установить модификатор доступа для методов public, а методы указывать со ссылкой на класс. Для приведенного выше примера это может иметь такие формы:
• При обычном объявлении:
ArProc ap = new ArProc (DelegateTest.Calc_Sum);
• При групповом преобразовании и совмещении объявления объекта типа делегата со ссылкой на метод:
ArProc ap = DelegateTest.Calc_Sum;
Групповая адресация
Можно создать список, состоящий из делегата с передаваемыми ему последовательно несколькими методами. Такой список называется групповой адресацией и позволяет создать цепочку вызовов методов, которые вызываются автоматически при обращении к делегату. Для добавления в цепочку делегата с другим методом применяют оператор +=, а для удаления оператор -=.
Методы из цепочки выполняются последовательно, и если эти методы возвращают значение, то смысла в таком возврате нет, так как каждый из экземпляров делегатов в цепочке будет переписывать возвращаемое значение. (Возвращаемое значение метода входит в сигнатуру делегата). Возвращено будет значение только для метода из последнего делегата в цепочке. Поэтому делегат, в котором используется групповая адресация, обычно имеет возвращаемый тип void.
Ниже приведен пример из книги ProfessorWEB (Руководство по C# Часть 2). Для изменения строки в памяти вызывающей части кода служит параметр типа ref. Благодаря такой связи методы оказываются более приспособленными для групповой адресации (связь с вызывающей программой через параметры, а тип возврата у методов void).
delegate void OpStroke(ref int[ ] arr);
public class ArrOperation
{
public static void WriteArray(ref int[ ] arr)
{
Console.WriteLine("Исходный массив: ");
foreach (int i in arr)
Console.Write("{0} ", i);
Console.WriteLine();
}
// Сортировка массива
public static void IncSort(ref int[ ] arr)
{
int j, k;
for (int i = 0; i < arr.Length - 1; i++)
{
j = 0;
do
{
if (arr[j] > arr[j + 1])
{
k = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = k;
}
j++;
} while (j < arr.Length - 1);
}
Console.WriteLine("Отсортированный массив по возрастанию: ");
foreach (int i in arr)
Console.Write("{0} ", i);
Console.WriteLine();
}
public static void DecSort(ref int[] arr)
{
int j, k;
for (int i = 0; i < arr.Length - 1; i++)
{
j = 0;
do
{
if (arr[j] < arr[j + 1])
{
k = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = k;
}
j++;
}
while (j < arr.Length - 1);
}
Console.WriteLine("Отсортированный массив по убыванию: ");
foreach (int i in arr)
Console.Write ("{0} ", i);
Console.WriteLine();
}
// Заменяем нечетные числа четными и наоборот
public static void ChetArr(ref int[] arr)
{
Console.WriteLine("Четный массив: ");
for (int i = 0; i < arr.Length; i++)
if (arr[i] % 2 != 0)
arr[i] += 1;
foreach (int i in arr)
Console.Write("{0} ", i);
Console.WriteLine();
}
public static void NeChetArr(ref int[] arr)
{
Console.WriteLine("Нечетный массив: ");
for (int i = 0; i < arr.Length; i++)
if (arr[i] % 2 == 0)
arr[i] += 1;
foreach (int i in arr)
Console.Write("{0} ", i);
Console.WriteLine();
}
}
class Program
{
static void Main()
{
int [ ] myArr = new int[6] { 2, -4, 10, 5, -6, 9 };
// Объявление переменной типа объявленного делегата (экземпляра)
OpStroke Del;
// Связываем методы из класса ArrOperation с делегатами
OpStroke Wr = ArrOperation.WriteArray;
OpStroke OnSortArr = ArrOperation.IncSort;
OpStroke OffSortArr = ArrOperation.DecSort;
OpStroke ChArr = ArrOperation.ChetArr;
OpStroke NeChArr = ArrOperation.NeChetArr;
// Групповая адресация
Del = Wr;
Del += OnSortArr;
Del += ChArr;
Del += OffSortArr;
Del += NeChArr;
// Выполняем делегат
Del(ref myArr);
Console.ReadLine();
}
}
Цепочки вызовов наиболее часто применяются для обработки событий.
Анонимные методы и лямбда-выражения
При объявлении объекта делегата описывается сигнатура делегата, а метод должен иметь объявление. Если делегат использует единственный метод, то допускается сокращенное оформление метода вместе с делегатом. Такое оформление называется анонимным методом. Синтаксис объявления объекта делегата с анонимным методом сохраняется. Разница возникает при оформлении этого делегата.
Ниже представлен пример программы, в которой делегат принимает единственный метод, вычисляющий число Фиббоначи с заданным номером. Сначала приведен пример классического оформления делегата.
class Program
{
delegate int Fib (int end);
public static int Fib_Calc(int num)
{
int prev = 1, curr = 1, next;
for (int k = 3; k <= num; ++k) // Цикл до введенного номера
{
next = prev + curr; // Очередное число
prev = curr; // Перестановка
curr = next;
}
return curr;
}
class AnonimDemo
{
static void Main()
{
int n = 7; //
// Объявление переменной типа делегата (экземпляра)
Fib fb = Fib_Calc;
Console.WriteLine("Число с номером {0} = {1} ", n, fb(n););
Console.ReadKey();
}
}
В примере:
Fib fb = Fib_Calc; // Объявление экземпляра делегата
fb(n) // Передача в экземпляр делегата параметра
Оформление той же программы с применением анонимного метода. Обратите внимание: вместо объявления переменной типа делегата использовано имя метода. После слова delegate указан список параметров, а затем оформлен метод, заканчивающийся точкой с запятой после закрывающей фигурной скобки.
class Program
{
delegate int Fib (int end);
class AnonimDemo
{
static void Main()
{
int n = 7; //
// Оформление анонимного метода, num = № числа
Fib Fib_Calc = delegate (int num)
{
int prev = 1, curr = 1, next;
for (int k = 3; k <= num; ++k) // Цикл до введенного номера
{
next = prev + curr; // Очередное число
prev = curr; // Перестановка
curr = next;
}
return curr;
};
Console.WriteLine("Число с номером {0} = {1} ", n, Fib_Calc(n));
Console.ReadKey();
}
}
}
Результат выполнения программы:
Здесь только одна строка для вызова метода
Fib_Calc(n)
При оформлении анонимного метода вся левая часть заголовка метода (тип возвращаемого значения и имя метода) заменена словом delegate. Круглые скобки содержат список параметров. В фигурных скобках записан код анонимного метода.
Для анонимных методов предложен упрощенный метод оформления, названный лямбда-выражениями. Так в предыдущем примере для перехода к лямбда-выраже-ниям достаточно строку:
Fib Fib_Calc = delegate (int num)
Заменить строкой:
Fib Fib_Calc = num => num
После знака => записывается одиночный оператор или блок операторов в фигурных скобках, а перед этим знаком записывается имя одиночного параметра (если он один), а если параметров несколько, то они записываются в круглых скобках через запятую.
Если перед некоторым классом, например, Product описать делегат:
delegate double Prod(double x, double y);
То лямбда-выражение можно оформить, например, так:
Prod pr = (x, y) => 2* x * y; // Оформление лямбда-выражения
Console.WriteLine(pr(3.5, 2.5) ) ; // Вызов делегата
Имена параметров при оформлении лямбда-выражений могут быть любыми, они подразумевают как бы неявное объявление параметров метода, поскольку их тип был задан при объявлении объекта класса делегата (типа делегата).
Делегаты могут использоваться в качестве параметров при вызове методов. В первом примере параграфа «Коллекции» был вызван метод поиска Find для списка. В качестве параметра для указания условий поиска указан делегат. Общий фрагмент оформления лямда-выражения имеет вид:
// Объявление блока и списка
Block blk = new Block();
List<Block> lst = new List<Block>() ;
Формат описания вызова метода Find в общем виде имеет форму:
Block List<Block>. Find(Predicate <Block> match)
Описание параметра, которое приводится после круглой скобки, следующей за словом Find, следующее:
Метод Find разыскивает элемент, который удовлетворяет условиям, заданным предикатом, и возвращает первое появление во всем списке List<T>, где match - (обобщенный) делегат Predicate<T>, который задает условие поиска для разыскиваемого элемента).
Ниже показана строка оформленного лямда-выражения для поиска:
Обратите внимание на имя параметра делегата. Это имя единственного параметра для делегата и может быть любым (но не должно совпадать с одним из имен, имеющихся в остальной части программы), поскольку справа от знака => находится оператор (задающий логическое условие), и область действия этого параметра ограничена этим оператором.
С делегатами связаны свойства ковариантности и контравариантности, которые позволяют вызывать методы с различающимися сигнатурами, если соответствующие классы являются производными. В частности, ковариантность позволяет передать делегату метод, который может возвращать класс, производный от класса, указанного в качестве возвращаемого при объявлении объекта типа делегата. А контравариантность позволяет использовать базовый класс вместо производного.
вставка
Понятие обобщенных делегатов
Язык C# позволяет объявлять обобщенные типы делегатов. В экземплярах такого типа делегатов указывается тип данных. Ниже приведен пример делегата, который вызывает любой метод, не возвращающий значение (тип возврата void) и имеющий в списке параметров один агрумент.
public delegate void GenDlgt<T>(T tp);
class Program
{
static public GenDlgt<string> ClType = new GenDlgt<string>(StrClass);
static void Main(string[] args)
{
// Объявление экземпляров делегатов для разных типов
ClType("Проверка обобщенного делегата");
GenDlgt<int> numb =
new GenDlgt<int>(NumbClass);
numb(6);
Console.ReadKey();
}
static void StrClass(string st)
{
Console.WriteLine("Длина строки = {0}
", st.Length);
}
static void NumbClass(int num)
{
int s = 0;
Console.WriteLine("Треугольные числа
");
for (int i = 1; i < num; ++i)
{
s += i;
Console.Write("{0} ", s);
}
}
Обратите внимание, что в GenDlgt <T> определен один параметр, предназначенный для передачи делегату класса, который будут обрабатывать методы. При объявлени экземпляра делегата этого типа необходимо указать значение типа класса вместе с именем метода, который может вызывать делегат. Например, если указан тип string, метод, выполняющий обработку, получит строковое значение.
Делегаты в C#
Лекции по предмету «Программирование»