Korzystanie z delegatów w C #


79

Czy możesz mi pomóc w zrozumieniu delegatów w języku C # i frameworku .NET? Próbowałem sprawdzić kod i stwierdziłem, że otrzymane wyniki były dla mnie nieoczekiwane. Oto ona:

class Program
{
    public static int I = 0;

    static Func<string> del = new Func<string>(I.ToString);

    static void Main(string[] args)
    {
        I = 10;
        Console.WriteLine("{0}", del());
    }
}

Odpowiedź brzmiała 0, ale nie 10. Dlaczego?


12
@Rotem: Nie, nie zrobił tego.
Daniel Hilgarth

3
@Rotem - Jest to deklaracja delegata. Dodanie ()wywołałoby ToString.
Oded

1
Przepraszam, nigdy nie użyłem Funcs, było to zgadywanie :)
Rotem

2
+1 za miłe pytanie, dobrze zadane. Świetny przykład tego, jak pozornie proste pytanie może uwydatnić słabo zrozumiały obszar języka / platformy.
Martin

5
Wystąpienie delegata (unicast) może wskazywać metodę wystąpienia lub staticmetodę. Gdy reprezentuje metodę instancji, delegat przechowuje zarówno obiekt docelowy, na którym ma zostać wywołana metoda, jak i informacje o metodzie. Więc kiedy mówisz del = I.ToString;, delbędzie trzymał obiekt, Iktóry jest tutaj Int32(niezmienny typ wartości). Gdy używasz funkcji anonimowej, del = () => I.ToString();kompilator tworzy metodę, static string xxx() { return I.ToString(); }a delobiekt przechowuje tę wygenerowaną metodę.
Jeppe Stig Nielsen

Odpowiedzi:


79

Powód jest następujący:

Sposób deklarowania delegata wskazuje bezpośrednio na ToStringmetodę statycznej instancji int. Jest schwytany w czasie stworzenia.

Jak Flindeberg wskazuje w komentarzach poniżej, każdy delegat ma cel i metodę, która ma zostać wykonana na miejscu docelowym.

W tym przypadku metodą do wykonania jest oczywiście ToStringmetoda. Interesującą częścią jest instancja, na której jest wykonywana metoda: jest to instancja Iw momencie tworzenia, co oznacza, że ​​delegat nie używa Iinstancji do użycia, ale przechowuje odwołanie do samej instancji.

Później zmieniasz Ina inną wartość, po prostu przypisując jej nową instancję. Nie zmienia to w magiczny sposób instancji przechwyconej w Twoim delegacie, dlaczego miałoby to robić?

Aby uzyskać oczekiwany wynik, musisz zmienić delegata na następujący:

static Func<string> del = new Func<string>(() => I.ToString());

W ten sposób delegat wskazuje na anonimową metodę, która jest wykonywana ToStringna bieżąco Iw momencie wykonywania delegata.

W tym przypadku metoda do wykonania jest metodą anonimową utworzoną w klasie, w której jest zadeklarowany delegat. Instancja ma wartość null, ponieważ jest metodą statyczną.

Przyjrzyj się kodowi, który kompilator generuje dla drugiej wersji delegata:

private static Func<string> del = new Func<string>(UserQuery.<.cctor>b__0);
private static string cctor>b__0()
{
    return UserQuery.I.ToString();
}

Jak widać, jest to normalna metoda, która coś robi . W naszym przypadku zwraca wynik wywołania ToStringbieżącego wystąpienia I.


1
@flindeberg: Możesz nawet użyć własnej klasy zamiast int. Będzie nadal zachowywał się tak samo, ponieważ podstawowa przyczyna nie zmienia się: delegat wskazuje na określone wcielenie ToString na jednym określonym obiekcie. Nie ma znaczenia, czy jest to typ referencyjny, czy typ wartości.
Daniel Hilgarth

3
@ user1859587: Delegat ma metodę i cel (instancję), ważne jest, czy przechwytuje twoją instancję lub instancję funkcji lambda, która z kolei zawiera odwołania do instancji.
flindeberg

1
@ user1859587: Nie ma za co. Przy okazji: próbowałem zaktualizować odpowiedź, aby było trochę jaśniejsze, co się tutaj dzieje. Możesz go ponownie przeczytać :-)
Daniel Hilgarth

3
Danielu, żeby potwierdzić komentarz Flindeberga: twoja odpowiedź jest poprawna, ale twoje komentarze dotyczące boksu nie. user1859587 ma rację: zaobserwowane zachowanie jest konsekwencją faktu, że delegat przechwytuje odbiorcę połączenia. Chociaż odbiorca wywołania ToString na int byłby odwołaniem do zmiennej int, delegat nie ma możliwości umieszczenia odwołania do zmiennej int na stercie; odniesienia do zmiennych mogą być przechowywane tylko tymczasowo. Robi więc następną najlepszą rzecz: opakowuje int i odwołuje się do tej lokalizacji sterty.
Eric Lippert

10
Ciekawą konsekwencją faktu, że odbiornik jest opakowany, jest to, że niemożliwe jest utworzenie delegata do GetValueOrDefault () w int nullable, ponieważ umieszczenie wartości nullable int tworzy int w pudełku, a nie int w pudełku, a int w pudełku ma brak metody GetValueOrDefault ().
Eric Lippert

4

Musisz przejść Ido swojej funkcji, aby I.ToString()mogła zostać wykonana w odpowiednim czasie (zamiast w momencie tworzenia funkcji).

class Program
{
    public static int I = 0;

    static Func<int, string> del = num => num.ToString();

    static void Main(string[] args)
    {
        I = 10;
        Console.WriteLine("{0}", del(I));
    }
}

1

Oto, jak należy to zrobić:

using System;

namespace ConsoleApplication1
{

    class Program
    {
        public static int I = 0;

        static Func<string> del = new Func<string>(() => {
            return I.ToString();
        });

        static void Main(string[] args)
        {
            I = 10;
            Console.WriteLine("{0}", del());
        }
    }
}


-2

Domyślam się, że int są przekazywane przez wartości, a nie odwołania, iz tego powodu podczas tworzenia delegata jest to delegat do metody ToString o bieżącej wartości „I” (0).


2
Twoje przypuszczenie nie jest poprawne. Nie ma to nic wspólnego z typami wartości i typami referencyjnymi. Dokładnie to samo stanie się z typami referencyjnymi.
Daniel Hilgarth

W rzeczywistości tak jest, jeśli na przykład użyjemy instancji klasy, a ToString manipuluje danymi instancji w celu zwrócenia wartości, zwróci to wartość bieżącego stanu klasy, a nie stan klasy w momencie tworzenia delegata. Funkcja nie działa podczas tworzenia delegata i istnieje tylko jedno wystąpienie klasy.
Yshayy

Func <string> (() => I.ToString ()) Powinien również działać, ponieważ nie używamy "I" do momentu wywołania metody.
Yshayy

Ale to nie jest odpowiednikiem tego, co się tutaj dzieje. Jeśli użyjesz klasy Foozamiast inti zmienisz wiersz I = 10na to, I = new Foo(10)uzyskasz dokładnie taki sam wynik, jak w przypadku bieżącego kodu. I.Value = 10jest czymś zupełnie innym. Nie powoduje to przypisania nowej instancji do I. Ale przypisanie nowej instancji do Ijest tutaj ważnym punktem.
Daniel Hilgarth

2
ok, więc problem polega na ponownym przypisaniu "I", gdyby "I" było zmienne i zmienilibyśmy obiekt bez ponownego przypisywania I, to by zadziałało. w tym przykładzie nie możemy tego zrobić, ponieważ I jest int (niezmienny).
Yshayy
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.