C# Language
Делегаты
Поиск…
замечания
Резюме
Тип делегата - это тип, представляющий конкретную подпись метода. Экземпляр этого типа относится к определенному методу с соответствующей сигнатурой. Параметры метода могут иметь типы делегирования, и поэтому этому одному методу передается ссылка на другой метод, который затем может быть вызван
Встроенные типы делегатов: Action<...>
, Predicate<T>
и Func<...,TResult>
System
имена содержат Action<...>
, Predicate<T>
и Func<...,TResult>
делегатов, где "..." составляет от 0 до 16 параметров универсального типа (для 0 параметров, Action
являются не- родовое).
Func
представляет методы с типом возвращаемого типа TResult
, а Action
представляет методы без возвращаемого значения (void). В обоих случаях дополнительные параметры типового типа соответствуют параметрам метода.
Predicate
представляет метод с булевым типом возврата, T - входной параметр.
Пользовательские типы делегатов
Именованные типы делегатов могут быть объявлены с использованием ключевого слова delegate
.
Вызов делегатов
Делегаты могут быть вызваны с использованием того же синтаксиса, что и методы: имя экземпляра делегата, за которым следуют скобки, содержащие любые параметры.
Присвоение делегатам
Делегатам можно назначить следующие способы:
- Назначение именованного метода
- Назначение анонимного метода с использованием лямбда
- Назначение именованного метода с использованием ключевого слова
delegate
.
Объединение делегатов
Несколько экземпляров делегата могут быть назначены одному экземпляру делегата с помощью оператора +
. Оператор -
может использоваться для удаления делегата компонента из другого делегата.
Базовые ссылки делегатов именованного метода
При назначении именованных методов делегатам они будут ссылаться на один и тот же базовый объект, если:
Они являются одним и тем же методом экземпляра, в том же экземпляре класса
Они представляют собой один и тот же статический метод для класса
public class Greeter { public void WriteInstance() { Console.WriteLine("Instance"); } public static void WriteStatic() { Console.WriteLine("Static"); } } // ... Greeter greeter1 = new Greeter(); Greeter greeter2 = new Greeter(); Action instance1 = greeter1.WriteInstance; Action instance2 = greeter2.WriteInstance; Action instance1Again = greeter1.WriteInstance; Console.WriteLine(instance1.Equals(instance2)); // False Console.WriteLine(instance1.Equals(instance1Again)); // True Action @static = Greeter.WriteStatic; Action staticAgain = Greeter.WriteStatic; Console.WriteLine(@static.Equals(staticAgain)); // True
Объявление типа делегата
Следующий синтаксис создает тип delegate
с именем NumberInOutDelegate
, представляющий метод, который принимает int
и возвращает int
.
public delegate int NumberInOutDelegate(int input);
Это можно использовать следующим образом:
public static class Program
{
static void Main()
{
NumberInOutDelegate square = MathDelegates.Square;
int answer1 = square(4);
Console.WriteLine(answer1); // Will output 16
NumberInOutDelegate cube = MathDelegates.Cube;
int answer2 = cube(4);
Console.WriteLine(answer2); // Will output 64
}
}
public static class MathDelegates
{
static int Square (int x)
{
return x*x;
}
static int Cube (int x)
{
return x*x*x;
}
}
example
экземпляра делегата выполняется так же, как метод Square
. Экземпляр делегата буквально выступает в качестве делегата для вызывающего: вызывающий вызывает делегата, а затем делегат вызывает целевой метод. Это направление отделяет вызывающего абонента от целевого метода.
Вы можете объявить общий тип делегата, и в этом случае вы можете указать, что тип является ковариантным ( out
) или контравариантным ( in
) в некоторых аргументах типа. Например:
public delegate TTo Converter<in TFrom, out TTo>(TFrom input);
Как и другие общие типы, общие типы делегатов могут иметь ограничения, например, where TFrom : struct, IConvertible where TTo : new()
.
Избегайте совместной и контравариантности для типов делегатов, которые предназначены для использования для делегатов многоадресной передачи, таких как типы обработчиков событий. Это связано с тем, что конкатенация ( +
) может завершиться неудачей, если тип выполнения отличается от типа времени компиляции из-за дисперсии. Например, избегайте:
public delegate void EventHandler<in TEventArgs>(object sender, TEventArgs e);
Вместо этого используйте инвариантный общий тип:
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
Также поддерживаются делегаты, где некоторые параметры изменяются с помощью ref
или out
, как в:
public delegate bool TryParser<T>(string input, out T result);
(пример использования TryParser<decimal> example = decimal.TryParse;
) или делегатов, где последний параметр имеет модификатор params
. Типы делегатов могут иметь необязательные параметры (значения по умолчанию для поставки). Типы делегатов могут использовать типы указателей, такие как int*
или char*
в своих подписях или типах возврата (используйте ключевое слово unsafe
). Тип делегата и его параметры могут нести пользовательские атрибуты.
Func , Действие и предикат типы делегатов
Пространство имен System содержит Func<..., TResult>
делегировать типы с 0 до 15 общих параметров, возвращая тип TResult
.
private void UseFunc(Func<string> func)
{
string output = func(); // Func with a single generic type parameter returns that type
Console.WriteLine(output);
}
private void UseFunc(Func<int, int, string> func)
{
string output = func(4, 2); // Func with multiple generic type parameters takes all but the first as parameters of that type
Console.WriteLine(output);
}
Пространство имен System также содержит типы делегатов Action<...>
с различным количеством общих параметров (от 0 до 16). Он похож на Func<T1, .., Tn>
, но он всегда возвращает void
.
private void UseAction(Action action)
{
action(); // The non-generic Action has no parameters
}
private void UseAction(Action<int, string> action)
{
action(4, "two"); // The generic action is invoked with parameters matching its type arguments
}
Predicate<T>
также является формой Func
но он всегда возвращает bool
. Предикат - это способ указания пользовательских критериев. В зависимости от значения входа и логики, определенных в предикате, он вернет либо true
либо false
. Таким образом, Predicate<T>
ведет себя так же, как Func<T, bool>
и оба могут быть инициализированы и использованы одинаково.
Predicate<string> predicate = s => s.StartsWith("a");
Func<string, bool> func = s => s.StartsWith("a");
// Both of these return true
var predicateReturnsTrue = predicate("abc");
var funcReturnsTrue = func("abc");
// Both of these return false
var predicateReturnsFalse = predicate("xyz");
var funcReturnsFalse = func("xyz");
Выбор того, следует ли использовать Predicate<T>
или Func<T, bool>
, действительно является вопросом мнения. Predicate<T>
, возможно, более выразителен в намерениях автора, тогда как Func<T, bool>
, скорее всего, будет знаком с большей частью разработчиков C #.
В дополнение к этому, есть некоторые случаи, когда доступен только один из вариантов, особенно при взаимодействии с другим API. Например, List<T>
и Array<T>
обычно используют Predicate<T>
для своих методов, в то время как большинство расширений LINQ принимают только Func<T, bool>
.
Назначение именованного метода делегату
Именованные методы могут быть назначены делегатам с соответствующими сигнатурами:
public static class Example
{
public static int AddOne(int input)
{
return input + 1;
}
}
Func<int,int> addOne = Example.AddOne
Example.AddOne
принимает int
и возвращает int
, его подпись соответствует делегату Func<int,int>
. Example.AddOne
может быть напрямую назначено addOne
поскольку они имеют соответствующие подписи.
Равенство делегатов
Вызов .Equals()
для делегата сравнивается по ссылочному равенству:
Action action1 = () => Console.WriteLine("Hello delegates");
Action action2 = () => Console.WriteLine("Hello delegates");
Action action1Again = action1;
Console.WriteLine(action1.Equals(action1)) // True
Console.WriteLine(action1.Equals(action2)) // False
Console.WriteLine(action1Again.Equals(action1)) // True
Эти правила также применяются при выполнении команды +=
или -=
в многоадресном делетете, например, при подписке и отмене подписки на события.
Назначение делегата лямбдой
Lambdas можно использовать для создания анонимных методов для назначения делегату:
Func<int,int> addOne = x => x+1;
Обратите внимание, что явное объявление типа требуется при создании переменной таким образом:
var addOne = x => x+1; // Does not work
Передача делегатов в качестве параметров
Делегаты могут использоваться в качестве типизированных указателей функций:
class FuncAsParameters
{
public void Run()
{
DoSomething(ErrorHandler1);
DoSomething(ErrorHandler2);
}
public bool ErrorHandler1(string message)
{
Console.WriteLine(message);
var shouldWeContinue = ...
return shouldWeContinue;
}
public bool ErrorHandler2(string message)
{
// ...Write message to file...
var shouldWeContinue = ...
return shouldWeContinue;
}
public void DoSomething(Func<string, bool> errorHandler)
{
// In here, we don't care what handler we got passed!
...
if (...error...)
{
if (!errorHandler("Some error occurred!"))
{
// The handler decided we can't continue
return;
}
}
}
}
Объединение делегатов (многоадресные делегаты)
Сложение +
и вычитание -
операции могут быть использованы для объединения экземпляров делегата. Делегат содержит список назначенных делегатов.
using System;
using System.Reflection;
using System.Reflection.Emit;
namespace DelegatesExample {
class MainClass {
private delegate void MyDelegate(int a);
private static void PrintInt(int a) {
Console.WriteLine(a);
}
private static void PrintType<T>(T a) {
Console.WriteLine(a.GetType());
}
public static void Main (string[] args)
{
MyDelegate d1 = PrintInt;
MyDelegate d2 = PrintType;
// Output:
// 1
d1(1);
// Output:
// System.Int32
d2(1);
MyDelegate d3 = d1 + d2;
// Output:
// 1
// System.Int32
d3(1);
MyDelegate d4 = d3 - d2;
// Output:
// 1
d4(1);
// Output:
// True
Console.WriteLine(d1 == d4);
}
}
}
В этом примере d3
представляет собой комбинацию делегатов d1
и d2
, поэтому при вызове программа выводит как строки 1
и System.Int32
.
Объединение делегатов с непустыми возвращаемыми типами:
Если групповой делегат имеет nonvoid
типа возвращаемого, вызывающий абонент получает возвращаемое значение из последнего метода , который будет вызвано. Предыдущие методы все еще вызываются, но их возвращаемые значения отбрасываются.
class Program
{
public delegate int Transformer(int x);
static void Main(string[] args)
{
Transformer t = Square;
t += Cube;
Console.WriteLine(t(2)); // O/P 8
}
static int Square(int x) { return x * x; }
static int Cube(int x) { return x*x*x; }
}
t(2)
вызовет первый Square
а затем Cube
. Возвращаемое значение Square отбрасывается и возвращается значение последнего метода, т.е. Cube
сохраняется.
Деактивировать многоадресную рассылку
Когда-либо хотелось вызвать делегата многоадресной рассылки, но вы хотите, чтобы весь список вызовов был вызван, даже если исключение происходит в любом из цепочки. Тогда вам повезло, я создал метод расширения, который делает именно это, бросая AggregateException
только после завершения полного списка:
public static class DelegateExtensions
{
public static void SafeInvoke(this Delegate del,params object[] args)
{
var exceptions = new List<Exception>();
foreach (var handler in del.GetInvocationList())
{
try
{
handler.Method.Invoke(handler.Target, args);
}
catch (Exception ex)
{
exceptions.Add(ex);
}
}
if(exceptions.Any())
{
throw new AggregateException(exceptions);
}
}
}
public class Test
{
public delegate void SampleDelegate();
public void Run()
{
SampleDelegate delegateInstance = this.Target2;
delegateInstance += this.Target1;
try
{
delegateInstance.SafeInvoke();
}
catch(AggregateException ex)
{
// Do any exception handling here
}
}
private void Target1()
{
Console.WriteLine("Target 1 executed");
}
private void Target2()
{
Console.WriteLine("Target 2 executed");
throw new Exception();
}
}
Эти результаты:
Target 2 executed
Target 1 executed
Вызов напрямую, без SaveInvoke
, выполнит только Target 2.
Закрытие внутри делегата
Закрытие - это встроенные анонимные методы, которые имеют возможность использовать переменные метода Parent
и другие анонимные методы, которые определены в области родителя.
По сути, закрытие представляет собой блок кода, который может быть выполнен в более позднее время, но который поддерживает среду, в которой он был сначала создан, т. Е. Он все еще может использовать локальные переменные и т. Д. Метода, который его создал, даже после этого метод завершил выполнение. - Джон Скит
delegate int testDel();
static void Main(string[] args)
{
int foo = 4;
testDel myClosure = delegate()
{
return foo;
};
int bar = myClosure();
}
Пример, взятый из Closures в .NET .
Инкапсулирующие преобразования в функциях
public class MyObject{
public DateTime? TestDate { get; set; }
public Func<MyObject, bool> DateIsValid = myObject => myObject.TestDate.HasValue && myObject.TestDate > DateTime.Now;
public void DoSomething(){
//We can do this:
if(this.TestDate.HasValue && this.TestDate > DateTime.Now){
CallAnotherMethod();
}
//or this:
if(DateIsValid(this)){
CallAnotherMethod();
}
}
}
В духе чистого кодирования инкапсуляция проверок и преобразований, подобных описанной выше как Func, может упростить чтение и понимание кода. В то время как приведенный выше пример очень прост, что, если было несколько свойств DateTime, каждый из которых имеет свои собственные правила проверки достоверности, и мы хотели проверить разные комбинации? Простые однострочные Funcs, у которых установлена логика возврата, могут быть как читаемыми, так и уменьшать кажущуюся сложность вашего кода. Рассмотрим приведенные ниже вызовы Func и представьте, сколько еще кода будет загромождать метод:
public void CheckForIntegrity(){
if(ShipDateIsValid(this) && TestResultsHaveBeenIssued(this) && !TestResultsFail(this)){
SendPassingTestNotification();
}
}