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


Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow