C# Language
I delegati
Ricerca…
Osservazioni
Sommario
Un tipo delegato è un tipo che rappresenta una particolare firma del metodo. Un'istanza di questo tipo si riferisce a un metodo particolare con una firma corrispondente. I parametri del metodo possono avere tipi delegati e quindi questo metodo deve essere passato un riferimento a un altro metodo, che può quindi essere invocato
Tipi di delegati integrati: Action<...>
, Predicate<T>
e Func<...,TResult>
Il namespace System
contiene Action<...>
, Predicate<T>
e Func<...,TResult>
delegates, dove "..." rappresenta tra 0 e 16 parametri di tipo generico (per 0 parametri, Action
is not- generico).
Func
rappresenta i metodi con un tipo di ritorno che corrisponde a TResult
e Action
rappresenta i metodi senza un valore di ritorno (void). In entrambi i casi, i parametri di tipo generico aggiuntivi corrispondono, in ordine, ai parametri del metodo.
Predicate
rappresenta un metodo con tipo di ritorno booleano, T è un parametro di input.
Tipi di delegati personalizzati
I tipi di delegato con nome possono essere dichiarati utilizzando la parola chiave delegate
.
Invocazione di delegati
I delegati possono essere richiamati usando la stessa sintassi dei metodi: il nome dell'istanza delegata, seguito da parentesi contenenti qualsiasi parametro.
Assegnazione ai delegati
I delegati possono essere assegnati nei seguenti modi:
- Assegnazione di un metodo denominato
- Assegnare un metodo anonimo usando un lambda
- Assegnazione di un metodo denominato utilizzando la parola chiave
delegate
.
Combinare i delegati
Più oggetti delegati possono essere assegnati a un'istanza delegata utilizzando l'operatore +
. L'operatore -
può essere utilizzato per rimuovere un componente delegato da un altro delegato.
Riferimenti sottostanti dei delegati del metodo denominato
Quando si assegnano metodi denominati a delegati, si riferiscono allo stesso oggetto sottostante se:
Sono lo stesso metodo di istanza, nella stessa istanza di una classe
Sono lo stesso metodo statico su una classe
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
Dichiarazione di un tipo di delegato
La seguente sintassi crea un tipo delegate
con nome NumberInOutDelegate
, che rappresenta un metodo che accetta un int
e restituisce un int
.
public delegate int NumberInOutDelegate(int input);
Questo può essere usato come segue:
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;
}
}
L' example
esempio delegato è eseguito nello stesso modo del Square
metodo. Un'istanza delegata funge letteralmente da delegato per il chiamante: il chiamante richiama il delegato, quindi il delegato chiama il metodo di destinazione. Questo indiretto disaccoppia il chiamante dal metodo di destinazione.
È possibile dichiarare un tipo di delegato generico e in tal caso è possibile specificare che il tipo è covariant ( out
) o controvariante ( in
) in alcuni argomenti di tipo. Per esempio:
public delegate TTo Converter<in TFrom, out TTo>(TFrom input);
Come altri tipi generici, i tipi di delegati generici possono avere vincoli, come nel where TFrom : struct, IConvertible where TTo : new()
.
Evitare la co- e la contravarianza per i tipi di delegati che devono essere utilizzati per i delegati multicast, come i tipi di gestori di eventi. Questo perché la concatenazione ( +
) può fallire se il tipo di runtime è diverso dal tipo in fase di compilazione a causa della varianza. Ad esempio, evitare:
public delegate void EventHandler<in TEventArgs>(object sender, TEventArgs e);
Invece, usa un tipo generico invariante:
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
Sono supportati anche i delegati in cui alcuni parametri sono modificati da ref
o out
, come in:
public delegate bool TryParser<T>(string input, out T result);
(Esempio: TryParser<decimal> example = decimal.TryParse;
), o delegati in cui l'ultimo parametro ha il modificatore params
. I tipi di delegati possono avere parametri opzionali (fornire valori predefiniti). I tipi di delegati possono usare tipi di puntatore come int*
o char*
nelle loro firme o tipi di ritorno (usa una parola chiave unsafe
). Un tipo di delegato e i relativi parametri possono contenere attributi personalizzati.
Il Func , Azione e Predicato tipi di delegati
Il namespace System contiene Func<..., TResult>
tipi di delegati con tra 0 e 15 parametri generici, restituendo il tipo 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);
}
Il namespace System contiene anche i tipi di Action<...>
delegate con un numero diverso di parametri generici (da 0 a 16). È simile a Func<T1, .., Tn>
, ma restituisce sempre 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>
è anche una forma di Func
ma restituirà sempre bool
. Un predicato è un modo per specificare un criterio personalizzato. A seconda del valore dell'input e della logica definita all'interno del predicato, restituirà true
o false
. Predicate<T>
si comporta quindi allo stesso modo di Func<T, bool>
ed entrambi possono essere inizializzati e utilizzati allo stesso modo.
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");
La scelta se utilizzare Predicate<T>
o Func<T, bool>
è davvero una questione di opinione. Predicate<T>
è probabilmente più espressivo delle intenzioni dell'autore, mentre Func<T, bool>
è probabilmente familiare a una percentuale maggiore di sviluppatori C #.
In aggiunta a ciò, ci sono alcuni casi in cui è disponibile solo una delle opzioni, specialmente quando si interagisce con un'altra API. Ad esempio, List<T>
e Array<T>
genere accettano Predicate<T>
per i loro metodi, mentre la maggior parte delle estensioni LINQ accettano solo Func<T, bool>
.
Assegnazione di un metodo denominato a un delegato
I metodi con nome possono essere assegnati ai delegati con firme corrispondenti:
public static class Example
{
public static int AddOne(int input)
{
return input + 1;
}
}
Func<int,int> addOne = Example.AddOne
Example.AddOne
prende un int
e restituisce un int
, la sua firma corrisponde al delegato Func<int,int>
. Example.AddOne
può essere assegnato direttamente ad addOne
perché hanno firme corrispondenti.
Delegare l'uguaglianza
Chiamare .Equals()
su un delegato viene confrontato per uguaglianza di riferimento:
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
Queste regole si applicano anche quando si fa +=
o -=
su un delegato multicast, ad esempio quando si sottoscrive e si annulla la sottoscrizione dagli eventi.
Assegnazione a un delegato di lambda
Lambdas può essere utilizzato per creare metodi anonimi da assegnare a un delegato:
Func<int,int> addOne = x => x+1;
Si noti che la dichiarazione esplicita di tipo è richiesta quando si crea una variabile in questo modo:
var addOne = x => x+1; // Does not work
Passando delegati come parametri
I delegati possono essere utilizzati come puntatori di funzione tipizzati:
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;
}
}
}
}
Combina delegati (delegati multicast)
Addizione +
e sottrazione -
operazioni possono essere utilizzate per combinare le istanze delegate. Il delegato contiene un elenco dei delegati assegnati.
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);
}
}
}
In questo esempio d3
è una combinazione di delegati d1
e d2
, quindi quando viene chiamato il programma emette sia 1
che le stringhe System.Int32
.
Combinazione di delegati con tipi di reso non void :
Se un delegato multicast ha un tipo di nonvoid
, il chiamante riceve il valore di ritorno dall'ultimo metodo da richiamare. I metodi precedenti sono ancora chiamati, ma i loro valori di ritorno sono scartati.
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)
chiamerà prima Square
e poi Cube
. Il valore di ritorno di Square viene scartato e restituisce il valore dell'ultimo metodo, ovvero Cube
viene mantenuto.
Sicuro invocare delegato multicast
Hai mai desiderato chiamare un delegato multicast ma vuoi che venga chiamato l'intero elenco di invocazioni anche se si verifica un'eccezione in una catena qualsiasi. Quindi sei fortunato, ho creato un metodo di estensione che fa proprio questo, lanciando un AggregateException
solo dopo che l'esecuzione dell'intero elenco è stata completata:
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();
}
}
Questo produce:
Target 2 executed
Target 1 executed
SaveInvoke
direttamente, senza SaveInvoke
, eseguirà solo Target 2.
Chiusura in un delegato
Le chiusure sono metodi anonimi incorporati che hanno la possibilità di utilizzare le variabili del metodo Parent
e altri metodi anonimi che sono definiti nell'ambito del genitore.
In sostanza, una chiusura è un blocco di codice che può essere eseguito in un secondo momento, ma che mantiene l'ambiente in cui è stato creato, cioè può ancora utilizzare le variabili locali ecc del metodo che lo ha creato, anche dopo il metodo ha terminato l'esecuzione. - Jon Skeet
delegate int testDel();
static void Main(string[] args)
{
int foo = 4;
testDel myClosure = delegate()
{
return foo;
};
int bar = myClosure();
}
Esempio tratto da Closures in .NET .
Incapsulando trasformazioni in funzioni
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();
}
}
}
Nello spirito di una codifica pulita, l'incapsulamento di verifiche e trasformazioni come quella di cui sopra come Func può facilitare la lettura e la comprensione del codice. Mentre l'esempio sopra è molto semplice, e se ci fossero più proprietà DateTime ognuna con le proprie regole di convalida diverse e volessimo controllare diverse combinazioni? Semplici funzioni di una riga che ciascuna ha stabilito la logica di ritorno possono essere entrambe leggibili e ridurre l'apparente complessità del codice. Considera le chiamate di Func qui sotto e immagina quanto più codice ingombrerebbe il metodo:
public void CheckForIntegrity(){
if(ShipDateIsValid(this) && TestResultsHaveBeenIssued(this) && !TestResultsFail(this)){
SendPassingTestNotification();
}
}