При разработке приложений с использованием сторонних библиотек мы будем довольно часто сталкиваться со стандартными делегатами, основными из которых являются три — Action
, Predicate
и Func
. Также, использование стандартных делегатов, в некоторых случаях, позволяет немного сократить свой исходный код. Рассмотрим, что из себя представляют стандартные делегаты Action
, Predicate
и Func
.
Делегат Action (действие)
Этот делегат инкапсулирует метод, который не возвращает значений. Такой делегат обычно используется для передачи метода в котором производятся какие-либо действия, но сам метод не возвращает никаких значений. Описание такого делегата выглядит следующим образом:
public delegate void Action();
По сути, этот делегат ничем не отличается от делегата, который мы создавали самостоятельно в самой первой части главы про делегаты. Наиболее интересными для нас являются универсальные делегаты Action, которые выглядят следующим образом:
public delegate void Action<in T>(T obj); public delegate void Action<in T1,in T2>(T1 arg1, T2 arg2); //... public delegate void Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16>
такие делегаты принимают от одного до шестнадцати параметров. Например, в одной из частей этой главы мы разрабатывали класс калькулятора, который выглядел вот так:
public delegate void Action(Calculator calc); public delegate double Operation(double x, double y); public class Calculator { public double X { get; set; } public double Y { get; set; } Operation? operation; public void Calculate(OperationType type) { operation = SetOperation(type); Console.WriteLine(operation(X, Y)); } private static Operation SetOperation(OperationType type) { switch (type) { case OperationType.Add: return (x, y) => x + y; case OperationType.Multiple: return (x, y) => x * y; case OperationType.Divide: return (x, y) => x / y; default: return (x, y) => (x + y); } } public void Configure(Action configure) { configure(this); } }
В методе Configure()
мы использовали делегат, который как раз-таки ничего не возвращает, но принимает один параметр — объект типа Calculator
. Следовательно, ничего нам не мешает воспользоваться универсальным делегатом Action<T>
и переписать код метода Configure()
следующим образом:
public void Configure(Action<Calculator> configure) { configure(this); }
при этом удалив объявление своего делегата Action(Calculator calc)
так как он нам уж не потребуется. Аналогичным образом мы можем использовать и другие варианты делегата Action
.
Делегат Predicate
Этот делегат принимает один параметр и всегда возвращает значение bool
:
public delegate bool Predicate<in T>(T obj);
Такой делегат удобно использовать для сравнения каких-либо значений. Например, воспользуемся этим делегатом в нашем калькуляторе:
Calculator calculator = new Calculator(); calculator.Configure((calc) => { calc.X = 10; calc.Y = 0; }); calculator.Calculate(OperationType.Add, calc => true); calculator.Calculate(OperationType.Multiple, calc => true); calculator.Calculate(OperationType.Divide, calc => calc.Y > 0); public enum OperationType { Add, Multiple, Divide} public class Calculator { public double X { get; set; } public double Y { get; set; } Operation? operation; public void Calculate(OperationType type, Predicate<Calculator> canCalculate) { if (canCalculate(this)) { operation = SetOperation(type); Console.WriteLine(operation(X, Y)); } else { Console.WriteLine("Невозможно выполнить операцию"); } } private static Operation SetOperation(OperationType type) { switch (type) { case OperationType.Add: return (x, y) => x + y; case OperationType.Multiple: return (x, y) => x * y; case OperationType.Divide: return (x, y) => x / y; default: return (x, y) => (x + y); } } public void Configure(Action<Calculator> configure) { configure(this); } }
Здесь метод Calculate()
в качестве второго параметра принимает делегат Predicate<Calculator>
. Так как у нас в калькуляторе выполняются действия сложения, умножения и деления, то нам необходимо каким-либо образом проверить не произойдет ли при выполнении операции какой-лио ошибки. В нашем случае — деления на ноль. Исходя из результата, который вернет нам делегат, мы либо выполняем расчёт, либо выводим сообщение об ошибке:
public void Calculate(OperationType type, Predicate<Calculator> canCalculate) { if (canCalculate(this)) { operation = SetOperation(type); Console.WriteLine(operation(X, Y)); } else { Console.WriteLine("Невозможно выполнить операцию"); } }
При вызове этого метода мы передаем во втором параметре лямбда-выражения, которые и будут преобразованы в тип делегата:
calculator.Calculate(OperationType.Add, calc => true); //сложение пройдет без ошибки calculator.Calculate(OperationType.Multiple, calc => true); //умножить мы также можем любые вещественные числа calculator.Calculate(OperationType.Divide, calc => calc.Y > 0);//на ноль делить не можем - проверяем значение Y
Теперь, если запустить приложение, то мы увидим следующий вывод в консоли:
0
Невозможно выполнить операцию
Делегат Func
Такой делегат может принимать от 0 до шестнадцати параметров и возвращает какое-либо значение.
public delegate TResult Func<out TResult>(); public delegate TResult Func<in T,out TResult>(T arg); //... public delegate TResult Func<in T1,in T2,in T3,in T4,in T5,in T6,in T7,in T8,in T9,in T10,in T11,in T12,in T13,in T14,in T15,in T16,out TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15, T16 arg16);
Первый универсальный параметр в таком делегате — это тип возвращаемого значения. Пример использования этого делегата можно продемонстрировать, опять же, на примере нашего калькулятора:
public class Calculator { public double X { get; set; } public double Y { get; set; } Func<double, double, double> operation; public void Calculate(OperationType type, Predicate<Calculator> canCalculate) { if (canCalculate(this)) { operation = SetOperation(type); Console.WriteLine(operation(X, Y)); } else { Console.WriteLine("Невозможно выполнить операцию"); } } private static Func<double, double, double> SetOperation(OperationType type) { switch (type) { case OperationType.Add: return (x, y) => x + y; case OperationType.Multiple: return (x, y) => x * y; case OperationType.Divide: return (x, y) => x / y; default: return (x, y) => (x + y); } } public void Configure(Action<Calculator> configure) { configure(this); } }
Так как наш калькулятор всегда оперирует двумя числами и возвращает третье, то здесь мы можем воспользоваться делегатом Func
, который имеет следующее описание:
public delegate TResult Func<in T1,in T2,out TResult>(T1 arg1, T2 arg2);
Этот тип делегата мы используем для задания операции:
Func<double, double, double> operation;
и, следовательно, как результат метода SetOperation()
:
private static Func<double, double, double> SetOperation(OperationType type) { switch (type) { case OperationType.Add: return (x, y) => x + y; case OperationType.Multiple: return (x, y) => x * y; case OperationType.Divide: return (x, y) => x / y; default: return (x, y) => (x + y); } }
Таким образом мы заменили все наши типы делегатов на универсальные делегаты, уже имеющиеся в .NET. При этом, на работе приложения такая замена никак не отразилась, но код приложения стал более коротким.
Итого
В .NET определен ряд стандартных делегатов, которые мы можем использовать при разработке своих приложений — это делегаты Action
, Predicate
и Func
. Делегаты Action
и Func
могут принимать от ноля до шестнадцати параметров. Стандартные делегаты активно используются в различных библиотеках C#. Как мы увидим далее, такие делегаты нашли широкое применение в таком механизме как LINQ.