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();
    }
}


Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow