Verwenden von Delegaten in C #


79

Können Sie mir in C # -Sprache und .NET Framework helfen, Delegaten zu verstehen? Ich habe versucht, einen Code zu überprüfen, und festgestellt, dass die Ergebnisse, die ich erhalten habe, für mich unerwartet waren. Hier ist es:

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

Die Antwort war 0, aber nicht 10. Warum?


12
@ Rotem: Nein, hat er nicht.
Daniel Hilgarth

3
@Rotem - Dies ist eine Delegatenerklärung. Hinzufügen ()würde aufrufen ToString.
Oded

1
Sorry, nie Funcs benutzt, war eine Vermutung :)
Rotem

2
+1 für eine nette Frage, gut gestellt. Ein gutes Beispiel dafür, wie eine scheinbar einfache Frage einen schlecht verstandenen Bereich der Sprache / Plattform hervorheben kann.
Martin

5
Eine (Unicast-) Delegateninstanz kann entweder auf eine Instanzmethode oder eine staticMethode verweisen . Wenn es sich um eine Instanzmethode handelt, enthält der Delegat sowohl das "Ziel" -Objekt, für das die Methode aufgerufen werden soll, als auch die Methodeninformationen. Wenn Sie also sagen del = I.ToString;, enthält der deldas Objekt, Idas hier ein Int32(unveränderlicher Werttyp) ist. Wenn Sie eine anonyme Funktion verwenden, erstellt del = () => I.ToString();der Compiler eine Methode static string xxx() { return I.ToString(); }und das delObjekt enthält diese generierte Methode.
Jeppe Stig Nielsen

Antworten:


79

Der Grund ist folgender:

Die Art und Weise, wie Sie den Delegaten deklarieren, verweist direkt auf die ToStringMethode der statischen int-Instanz. Es wird zum Zeitpunkt der Erstellung erfasst.

Wie flindeberg in den Kommentaren unten ausführt, hat jeder Delegierte ein Ziel und eine Methode, die auf dem Ziel ausgeführt werden soll.

In diesem Fall ist die auszuführende ToStringMethode offensichtlich die Methode. Der interessante Teil ist die Instanz, auf der die Methode ausgeführt wird: Es ist die Instanz Izum Zeitpunkt der Erstellung, was bedeutet, dass der Delegat Idie Instanz nicht verwendet , um sie zu verwenden, sondern den Verweis auf die Instanz selbst speichert.

Später wechseln Sie Izu einem anderen Wert und weisen ihm im Grunde eine neue Instanz zu. Dies ändert die in Ihrem Delegaten erfasste Instanz nicht auf magische Weise. Warum sollte dies der Fall sein?

Um das erwartete Ergebnis zu erzielen, müssen Sie den Delegaten folgendermaßen ändern:

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

Auf diese Weise verweist der Delegat auf eine anonyme Methode, die zum Zeitpunkt der Ausführung des Delegaten ToStringauf dem aktuellen Stand ausgeführt wird I.

In diesem Fall ist die auszuführende Methode eine anonyme Methode, die in der Klasse erstellt wurde, in der der Delegat deklariert ist. Die Instanz ist null, da es sich um eine statische Methode handelt.

Sehen Sie sich den Code an, den der Compiler für die zweite Version des Delegaten generiert:

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

Wie Sie sehen können, ist es eine normale Methode, die etwas tut . In unserem Fall wird das Ergebnis des Aufrufs ToStringder aktuellen Instanz von zurückgegeben I.


1
@flindeberg: Du kannst sogar deine eigene Klasse anstelle von int verwenden. Es wird sich immer noch genauso verhalten, da sich der zugrunde liegende Grund nicht ändert: Der Delegat zeigt auf die spezifische Inkarnation von ToString für ein bestimmtes Objekt. Es spielt keine Rolle, ob dies ein Referenztyp oder ein Werttyp ist.
Daniel Hilgarth

3
@ user1859587: Ein Delegat hat eine Methode und ein Ziel (Instanz). Es ist wichtig, ob er Ihre Instanz erfasst oder ob die Instanz der Lambda-Funktion wiederum Verweise auf die Instanz enthält.
Flindeberg

1
@ user1859587: Gern geschehen. Übrigens: Ich habe versucht, die Antwort zu aktualisieren, um etwas klarer zu machen, was hier vor sich geht. Vielleicht möchten Sie es noch einmal lesen :-)
Daniel Hilgarth

3
Daniel, nur um flindebergs Kommentar zu bestätigen: Ihre Antwort ist korrekt, Ihre Kommentare zum Boxen jedoch nicht. user1859587 ist korrekt: Das beobachtete Verhalten ist eine Folge der Tatsache, dass der Delegat den Empfänger des Anrufs erfasst. Obwohl der Empfänger eines Aufrufs von ToString auf int eine Referenz auf eine int-Variable wäre, hat der Delegat keine Möglichkeit, eine Referenz auf eine int-Variable auf dem Heap zu platzieren. Verweise auf Variablen werden möglicherweise nur vorübergehend gespeichert. Es macht also das nächstbeste: Es boxt das int und verweist auf diesen Heap-Speicherort.
Eric Lippert

10
Eine interessante Konsequenz der Tatsache, dass der Empfänger eingerahmt ist, ist, dass es unmöglich ist, einen Delegaten für GetValueOrDefault () auf einem nullbaren Int zu erstellen, da das Boxen eines nullfähigen Int ein boxed int erzeugt, kein boxed nullable int, und ein boxed int hat keine GetValueOrDefault () -Methode.
Eric Lippert

4

Sie müssen an IIhre Funktion übergeben, damit diese I.ToString()zum richtigen Zeitpunkt ausgeführt werden kann (anstatt zum Zeitpunkt der Erstellung der Funktion).

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

So sollte das gemacht werden:

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

Ich vermute, dass int von Werten übergeben wird, die keine Referenzen sind. Aus diesem Grund ist es beim Erstellen des Delegaten ein Delegat für die Methode ToString des aktuellen Werts von "I" (0).


2
Ihre Vermutung ist nicht richtig. Dies hat nichts mit Werttypen oder Referenztypen zu tun. Genau das Gleiche würde auch bei Referenztypen passieren.
Daniel Hilgarth

Wenn wir beispielsweise eine Klasseninstanz verwenden und ToString die Instanzdaten für den Rückgabewert manipuliert, wird der Rückgabewert des aktuellen Klassenstatus und nicht der Status der Klasse beim Erstellen des Delegaten erzeugt. Die Funktion wird nicht ausgeführt, wenn der Delegat erstellt wird und es nur eine Instanz der Klasse gibt.
Yshayy

Func <string> (() => I.ToString ()) Sollte auch funktionieren, da wir "I" erst beim Aufruf der Methode verwenden.
Yshayy

Aber das ist nicht gleichbedeutend mit dem, was hier vor sich geht. Wenn Sie die Klasse Fooanstelle von verwenden intund die Zeile I = 10in ändern I = new Foo(10)würden, hätten Sie genau das gleiche Ergebnis wie mit dem aktuellen Code. I.Value = 10ist etwas ganz anderes. Dies weist keine neue Instanz zu I. Das Zuweisen einer neuen Instanz Iist hier jedoch der wichtige Punkt.
Daniel Hilgarth

2
ok, das Problem besteht also darin, "I" neu zuzuweisen. Wenn "I" veränderlich wäre und wir das Objekt ändern, ohne I neu zuzuweisen, hätte es funktioniert. In diesem Beispiel können wir das nicht tun, weil ich int (unveränderlich) bin.
Yshayy
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.