C# Language
Delegaci
Szukaj…
Uwagi
streszczenie
Typ delegowany to typ reprezentujący określoną sygnaturę metody. Instancja tego typu odnosi się do konkretnej metody z pasującym podpisem. Parametry metody mogą mieć typy delegowane, a zatem ta jedna metoda ma zostać przekazana referencją do innej metody, którą można następnie wywołać
Wbudowane typy delegatów: Action<...>
, Predicate<T>
i Func<...,TResult>
System
przestrzeń nazw zawiera delegatów Action<...>
, Predicate<T>
i Func<...,TResult>
, gdzie „...” reprezentuje od 0 do 16 ogólnych parametrów typu (dla parametrów 0 Action
nie jest rodzajowy).
Func
reprezentuje metody o typie zwracanym pasującym do TResult
, a Action
reprezentuje metody bez wartości zwracanej (void). W obu przypadkach dodatkowe parametry typu ogólnego odpowiadają, w kolejności, parametrom metody.
Predicate
reprezentuje metodę z logicznym typem powrotu, T jest parametrem wejściowym.
Niestandardowe typy delegatów
Nazwane typy delegatów można zadeklarować za pomocą słowa kluczowego delegate
.
Wywoływanie delegatów
Delegaci mogą być wywoływani przy użyciu tej samej składni co metody: nazwa instancji delegowanej, po której następują nawiasy zawierające dowolne parametry.
Przypisywanie do delegatów
Delegatów można przypisać na następujące sposoby:
- Przypisywanie nazwanej metody
- Przypisywanie anonimowej metody za pomocą lambda
- Przypisywanie nazwanej metody za pomocą słowa kluczowego
delegate
.
Łączenie delegatów
Wiele obiektów delegowanych można przypisać do jednej instancji delegowanej za pomocą operatora +
. Za pomocą operatora -
można usunąć delegata komponentu z innego delegata.
Referencje bazowe nazwanych delegatów metod
Przypisując nazwane metody do delegatów, będą odnosić się do tego samego obiektu podstawowego, jeśli:
Są to te same metody instancji w tej samej instancji klasy
Są to ta sama metoda statyczna w klasie
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
Deklarowanie typu delegata
Poniższa składnia tworzy typ delegate
o nazwie NumberInOutDelegate
, reprezentujący metodę, która przyjmuje int
i zwraca int
.
public delegate int NumberInOutDelegate(int input);
Można to wykorzystać w następujący sposób:
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
instancja delegowana jest wykonywana w taki sam sposób, jak metoda Square
. Instancja delegata działa dosłownie jako delegat dla wywołującego: wywołujący wywołuje delegata, a następnie delegat wywołuje metodę docelową. Ta pośrednia funkcja oddziela osobę dzwoniącą od metody docelowej.
Możesz zadeklarować ogólny typ delegata, aw takim przypadku możesz określić, że typ jest kowariantny ( out
) lub contravariant ( in
) w niektórych argumentach typu. Na przykład:
public delegate TTo Converter<in TFrom, out TTo>(TFrom input);
Podobnie jak inne typy ogólne, ogólne typy delegatów mogą mieć ograniczenia, takie jak where TFrom : struct, IConvertible where TTo : new()
.
Unikaj ko-i sprzeczności dla typów delegatów, które mają być używane dla delegatów multiemisji, takich jak typy obsługi zdarzeń. Wynika to z faktu, że konkatenacja ( +
) może się nie powieść, jeśli typ wykonania różni się od typu kompilacji z powodu wariancji. Na przykład unikaj:
public delegate void EventHandler<in TEventArgs>(object sender, TEventArgs e);
Zamiast tego użyj niezmiennego typu ogólnego:
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
Obsługiwane są również delegaty, w których niektóre parametry są modyfikowane przez ref
lub out
, jak w:
public delegate bool TryParser<T>(string input, out T result);
(przykład użyj TryParser<decimal> example = decimal.TryParse;
) lub deleguje, w których ostatni parametr ma modyfikator params
. Typy uczestników mogą mieć parametry opcjonalne (podać wartości domyślne). Typy delegowane mogą używać w swoich podpisach typów wskaźników takich jak int*
lub char*
lub typów zwracanych (użyj unsafe
słowa kluczowego). Typ delegata i jego parametry mogą przenosić atrybuty niestandardowe.
The Func , Akcja i Predicate typy delegatów
Systemowa przestrzeń nazw zawiera typy delegatów Func<..., TResult>
z 0 do 15 parametrami ogólnymi, zwracając typ 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);
}
Systemowa przestrzeń nazw zawiera także typy delegowania Action<...>
o różnej liczbie parametrów ogólnych (od 0 do 16). Jest podobny do Func<T1, .., Tn>
, ale zawsze zwraca wartość 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>
jest również formą Func
ale zawsze zwróci bool
. Predykat to sposób określania niestandardowych kryteriów. W zależności od wartości danych wejściowych i logiki zdefiniowanej w predykacie, zwróci wartość true
lub false
. Predicate<T>
działa zatem w taki sam sposób jak Func<T, bool>
i oba mogą być inicjowane i używane w ten sam sposób.
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");
Wybór, czy użyć Predicate<T>
czy Func<T, bool>
jest naprawdę kwestią opinii. Predicate<T>
jest prawdopodobnie bardziej ekspresyjny w stosunku do intencji autora, podczas gdy Func<T, bool>
prawdopodobnie będzie znany większej części programistów C #.
Ponadto w niektórych przypadkach dostępna jest tylko jedna z opcji, szczególnie podczas interakcji z innym interfejsem API. Na przykład List<T>
i Array<T>
zazwyczaj przyjmują Predicate<T>
dla swoich metod, podczas gdy większość rozszerzeń LINQ akceptuje tylko Func<T, bool>
.
Przypisywanie nazwanej metody delegatowi
Nazwane metody można przypisać delegatom z pasującymi podpisami:
public static class Example
{
public static int AddOne(int input)
{
return input + 1;
}
}
Func<int,int> addOne = Example.AddOne
Example.AddOne
przyjmuje liczbę int
i zwraca liczbę int
, jej sygnatura odpowiada delegowanemu Func<int,int>
. Example.AddOne
można bezpośrednio przypisać do addOne
ponieważ mają one pasujące podpisy.
Delegować równość
Wywołanie funkcji .Equals()
na delegacie porównuje według równości odniesienia:
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
Te zasady obowiązują również podczas wykonywania +=
lub -=
na uczestniku multiemisji, na przykład podczas subskrybowania i anulowania subskrypcji zdarzeń.
Przypisywanie delegata przez lambda
Lambdas może służyć do tworzenia anonimowych metod przypisywania do delegata:
Func<int,int> addOne = x => x+1;
Pamiętaj, że przy tworzeniu zmiennej w ten sposób wymagana jest wyraźna deklaracja typu:
var addOne = x => x+1; // Does not work
Przekazywanie delegatów jako parametrów
Delegatów można używać jako wskaźników funkcji pisanych na maszynie:
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;
}
}
}
}
Połącz delegatów (delegaci multiemisji)
Dodawanie +
odejmowanie -
operacji można używać do łączenia instancji delegowanych. Delegat zawiera listę przypisanych delegatów.
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);
}
}
}
W tym przykładzie d3
jest kombinacją delegatów d1
i d2
, więc gdy jest nazywany, program generuje zarówno łańcuchy 1
jak i System.Int32
.
Łączenie delegatów z nieważnymi typami zwrotów:
Jeśli delegat multiemisji ma nonvoid
powrotu nonvoid
, program wywołujący otrzymuje wartość zwrotną z ostatniej metody, która ma zostać wywołana. Poprzednie metody są nadal wywoływane, ale ich zwracane wartości są odrzucane.
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)
wywoła najpierw Square
a następnie Cube
. Zwracana wartość kwadratu jest odrzucana, a zwracana jest wartość ostatniej metody, tj. Cube
.
Bezpieczne wywołanie delegata multiemisji
Kiedykolwiek chciałeś zadzwonić do delegata multiemisji, ale chcesz, aby cała lista wywołań była wywoływana, nawet jeśli wystąpi wyjątek w dowolnym łańcuchu. Więc masz szczęście, stworzyłem metodę rozszerzenia, która właśnie to robi, zgłaszając AggregateException
dopiero po zakończeniu wykonywania całej listy:
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();
}
}
To daje:
Target 2 executed
Target 1 executed
Wywołanie bezpośrednio, bez SaveInvoke
, SaveInvoke
tylko Cel 2.
Zamknięcie w delegacie
Zamknięcia są wbudowanymi anonimowymi metodami, które mają możliwość korzystania ze zmiennych metody Parent
i innych anonimowych metod zdefiniowanych w zakresie jednostki nadrzędnej.
Zasadniczo zamknięcie jest blokiem kodu, który można wykonać w późniejszym czasie, ale który utrzymuje środowisko, w którym został utworzony po raz pierwszy - tzn. Może nadal używać zmiennych lokalnych itp. Metody, która go utworzyła, nawet po tym metoda zakończyła wykonywanie. - Jon Skeet
delegate int testDel();
static void Main(string[] args)
{
int foo = 4;
testDel myClosure = delegate()
{
return foo;
};
int bar = myClosure();
}
Przykład zaczerpnięty z zamknięć w .NET .
Hermetyzacja transformacji w funcs
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();
}
}
}
W duchu czystego kodowania, hermetyzowanie kontroli i transformacji, takich jak powyższy jako Func, może ułatwić odczytanie i zrozumienie kodu. Chociaż powyższy przykład jest bardzo prosty, co zrobić, jeśli istnieje wiele właściwości DateTime z własnymi odmiennymi regułami sprawdzania poprawności, a my chcemy sprawdzić różne kombinacje? Proste, jednowierszowe funkcje, z których każda ma ustaloną logikę zwrotną, mogą być zarówno czytelne, jak i zmniejszają pozorną złożoność kodu. Rozważ poniższe wywołania Func i wyobraź sobie, o ile więcej kodu zaśmieca metoda:
public void CheckForIntegrity(){
if(ShipDateIsValid(this) && TestResultsHaveBeenIssued(this) && !TestResultsFail(this)){
SendPassingTestNotification();
}
}