Warum kann eine anonyme Methode in C # keine Yield-Anweisung enthalten?


87

Ich dachte, es wäre schön, so etwas zu tun (wobei das Lambda eine Rendite erzielt):

public IList<T> Find<T>(Expression<Func<T, bool>> expression) where T : class, new()
{
    IList<T> list = GetList<T>();
    var fun = expression.Compile();

    var items = () => {
        foreach (var item in list)
            if (fun.Invoke(item))
                yield return item; // This is not allowed by C#
    }

    return items.ToList();
}

Ich habe jedoch festgestellt, dass ich Yield nicht in anonymen Methoden verwenden kann. Ich frage mich warum. Die Ertragsdokumente sagen nur, dass es nicht erlaubt ist.

Da es nicht erlaubt war, habe ich einfach eine Liste erstellt und die Elemente hinzugefügt.


Jetzt, da wir in C # 5.0 anonyme asyncLambdas zulassen können await, würde mich interessieren, warum sie noch keine anonymen Iteratoren mit yieldInside implementiert haben . Mehr oder weniger ist es der gleiche Generator für Zustandsmaschinen.
Noseratio

Antworten:


112

Eric Lippert hat kürzlich eine Reihe von Blog-Posts darüber geschrieben, warum Erträge in einigen Fällen nicht zulässig sind.

EDIT2:

  • Teil 7 (dieser wurde später veröffentlicht und befasst sich speziell mit dieser Frage)

Dort finden Sie wahrscheinlich die Antwort ...


EDIT1: Dies wird in den Kommentaren von Teil 5 in Erics Antwort auf Abhijeet Patels Kommentar erklärt:

F:

Eric,

Können Sie auch einen Einblick geben, warum "Ausbeuten" in einer anonymen Methode oder einem Lambda-Ausdruck nicht zulässig sind?

EIN :

Gute Frage. Ich hätte gerne anonyme Iteratorblöcke. Es wäre absolut fantastisch, sich selbst einen kleinen Sequenzgenerator bauen zu lassen, der über lokalen Variablen geschlossen wird. Der Grund, warum nicht, ist einfach: Die Vorteile überwiegen nicht die Kosten. Die Großartigkeit, Sequenzgeneratoren an Ort und Stelle zu machen, ist im großen Schema der Dinge eigentlich ziemlich gering, und nominelle Methoden machen die Arbeit in den meisten Szenarien gut genug. Die Vorteile sind also nicht so überzeugend.

Die Kosten sind hoch. Das Umschreiben von Iteratoren ist die komplizierteste Transformation im Compiler, und das Umschreiben anonymer Methoden ist die zweitkomplizierteste. Anonyme Methoden können sich in anderen anonymen Methoden befinden, und anonyme Methoden können sich in Iteratorblöcken befinden. Daher schreiben wir zuerst alle anonymen Methoden neu, sodass sie zu Methoden einer Abschlussklasse werden. Dies ist das vorletzte, was der Compiler tut, bevor er IL für eine Methode ausgibt. Sobald dieser Schritt abgeschlossen ist, kann der Iterator-Umschreiber davon ausgehen, dass der Iteratorblock keine anonymen Methoden enthält. Sie wurden alle bereits neu geschrieben. Daher kann sich der Iterator-Umschreiber nur auf das Umschreiben des Iterators konzentrieren, ohne befürchten zu müssen, dass sich dort eine nicht realisierte anonyme Methode befindet.

Außerdem "verschachteln" Iteratorblöcke im Gegensatz zu anonymen Methoden niemals. Der Iterator-Umschreiber kann davon ausgehen, dass alle Iteratorblöcke "oberste Ebene" sind.

Wenn anonyme Methoden Iteratorblöcke enthalten dürfen, gehen beide Annahmen aus dem Fenster. Sie können einen Iteratorblock haben, der eine anonyme Methode enthält, die eine anonyme Methode enthält, die einen Iteratorblock enthält, der eine anonyme Methode enthält, und ... yuck. Jetzt müssen wir einen Umschreibungsdurchlauf schreiben, der verschachtelte Iteratorblöcke und verschachtelte anonyme Methoden gleichzeitig verarbeiten kann und unsere beiden kompliziertesten Algorithmen zu einem weitaus komplizierteren Algorithmus zusammenführt. Es wäre wirklich schwierig zu entwerfen, zu implementieren und zu testen. Wir sind klug genug, das zu tun, da bin ich mir sicher. Wir haben hier ein kluges Team. Aber wir wollen diese große Last nicht für eine "schön zu haben, aber nicht notwendig" -Funktion übernehmen. - Eric


2
Interessant, zumal es jetzt lokale Funktionen gibt.
Mafii

3
Ich frage mich, ob diese Antwort veraltet ist, weil sie in einer lokalen Funktion eine Rendite bringt.
Joshua

2
@Joshua, aber eine lokale Funktion ist nicht dasselbe wie eine anonyme Methode ... Yield Return ist in anonymen Methoden immer noch nicht zulässig.
Thomas Levesque

21

Eric Lippert hat eine hervorragende Artikelserie über die Einschränkungen (und Entwurfsentscheidungen, die diese Auswahl beeinflussen) für Iteratorblöcke geschrieben

Insbesondere Iteratorblöcke werden durch einige ausgefeilte Compiler-Code-Transformationen implementiert. Diese Transformationen würden sich auf die Transformationen auswirken, die in anonymen Funktionen oder Lambdas stattfinden, so dass beide unter bestimmten Umständen versuchen würden, den Code in ein anderes Konstrukt zu "konvertieren", das mit dem anderen nicht kompatibel ist.

Infolgedessen ist ihnen die Interaktion verboten.

Wie Iteratorblöcke unter der Haube funktionieren, wird hier gut behandelt .

Als einfaches Beispiel für eine Inkompatibilität:

public IList<T> GreaterThan<T>(T t)
{
    IList<T> list = GetList<T>();
    var items = () => {
        foreach (var item in list)
            if (fun.Invoke(item))
                yield return item; // This is not allowed by C#
    }

    return items.ToList();
}

Der Compiler möchte dies gleichzeitig in Folgendes konvertieren:

// inner class
private class Magic
{
    private T t;
    private IList<T> list;
    private Magic(List<T> list, T t) { this.list = list; this.t = t;}

    public IEnumerable<T> DoIt()
    {
        var items = () => {
            foreach (var item in list)
                if (fun.Invoke(item))
                    yield return item;
        }
    }
}

public IList<T> GreaterThan<T>(T t)
{
    var magic = new Magic(GetList<T>(), t)
    var items = magic.DoIt();
    return items.ToList();
}

Gleichzeitig versucht der Iterator-Aspekt, eine kleine Zustandsmaschine zu erstellen. Bestimmte einfache Beispiele funktionieren möglicherweise mit einer angemessenen Überprüfung der Integrität (zuerst mit den (möglicherweise willkürlich) verschachtelten Abschlüssen) und dann, ob die resultierenden Klassen der untersten Ebene in Iterator-Zustandsmaschinen umgewandelt werden können.

Dies wäre jedoch

  1. Ziemlich viel Arbeit.
  2. Könnte möglicherweise nicht in allen Fällen funktionieren, ohne dass zumindest der Iteratorblockaspekt verhindern könnte, dass der Abschlussaspekt bestimmte Transformationen aus Effizienzgründen anwendet (z. B. das Heraufstufen lokaler Variablen zu Instanzvariablen anstelle einer vollwertigen Abschlussklasse).
    • Wenn es sogar eine geringe Wahrscheinlichkeit einer Überlappung gäbe, bei der es unmöglich oder ausreichend schwierig wäre, nicht implementiert zu werden, wäre die Anzahl der daraus resultierenden Supportprobleme wahrscheinlich hoch, da die subtile Änderung bei vielen Benutzern verloren gehen würde.
  3. Es kann sehr leicht umgangen werden.

In Ihrem Beispiel so:

public IList<T> Find<T>(Expression<Func<T, bool>> expression) 
    where T : class, new()
{
    return FindInner(expression).ToList();
}

private IEnumerable<T> FindInner<T>(Expression<Func<T, bool>> expression) 
    where T : class, new()
{
    IList<T> list = GetList<T>();
    var fun = expression.Compile();
    foreach (var item in list)
        if (fun.Invoke(item))
            yield return item;
}

2
Es gibt keinen klaren Grund, warum der Compiler nicht die übliche Iterator-Transformation durchführen kann, nachdem er alle Abschlüsse aufgehoben hat. Kennen Sie einen Fall, der tatsächlich Schwierigkeiten bereiten würde? Übrigens sollte deine MagicKlasse sein Magic<T>.
Qwertie

3

Leider weiß ich nicht, warum sie das nicht zugelassen haben, da es natürlich durchaus möglich ist, sich vorzustellen, wie das funktionieren würde.

Anonyme Methoden sind jedoch bereits ein Stück "Compiler-Magie" in dem Sinne, dass die Methode entweder in eine Methode in der vorhandenen Klasse oder sogar in eine ganz neue Klasse extrahiert wird, je nachdem, ob es sich um lokale Variablen handelt oder nicht.

Darüber hinaus werden Iterator-Methoden yieldmit Compiler Magic implementiert.

Ich vermute, dass einer dieser beiden Codes den Code für das andere Stück Magie nicht identifizierbar macht und dass beschlossen wurde, keine Zeit darauf zu verwenden, dass dies für die aktuellen Versionen des C # -Compilers funktioniert. Natürlich ist es möglicherweise überhaupt keine bewusste Entscheidung, und dass es einfach nicht funktioniert, weil niemand daran gedacht hat, es umzusetzen.

Für eine 100% genaue Frage würde ich vorschlagen, dass Sie die Microsoft Connect- Website verwenden und eine Frage melden. Ich bin sicher, dass Sie im Gegenzug etwas Verwendbares erhalten.


1

Ich würde das tun:

IList<T> list = GetList<T>();
var fun = expression.Compile();

return list.Where(item => fun.Invoke(item)).ToList();

Natürlich benötigen Sie die System.Core.dll, auf die in .NET 3.5 verwiesen wird, für die Linq-Methode. Und umfassen:

using System.Linq;

Prost,

Schlau


0

Vielleicht ist es nur eine Syntaxbeschränkung. In Visual Basic .NET, das C # sehr ähnlich ist, ist das Schreiben durchaus möglich, obwohl es umständlich ist

Sub Main()
    Console.Write("x: ")
    Dim x = CInt(Console.ReadLine())
    For Each elem In Iterator Function()
                         Dim i = x
                         Do
                             Yield i
                             i += 1
                             x -= 1
                         Loop Until i = x + 20
                     End Function()
        Console.WriteLine($"{elem} to {x}")
    Next
    Console.ReadKey()
End Sub

Beachten Sie auch die Klammern ' here; die Lambda - Funktion Iterator Function... End Function gibt ein IEnumerable(Of Integer)aber ist nicht ein solches Objekt selbst. Es muss aufgerufen werden, um dieses Objekt zu erhalten.

Der konvertierte Code von [1] löst Fehler in C # 7.3 (CS0149) aus:

static void Main()
{
    Console.Write("x: ");
    var x = System.Convert.ToInt32(Console.ReadLine());
    // ERROR: CS0149 - Method name expected 
    foreach (var elem in () =>
    {
        var i = x;
        do
        {
            yield return i;
            i += 1;
            x -= 1;
        }
        while (!i == x + 20);
    }())
        Console.WriteLine($"{elem} to {x}");
    Console.ReadKey();
}

Ich stimme dem in den anderen Antworten angegebenen Grund nicht zu, dass es für den Compiler schwierig ist, damit umzugehen. Das Iterator Function()im VB.NET-Beispiel angezeigte Beispiel wurde speziell für Lambda-Iteratoren erstellt.

In VB gibt es das IteratorSchlüsselwort; es hat kein C # Gegenstück. IMHO gibt es keinen wirklichen Grund, warum dies kein Merkmal von C # ist.

Wenn Sie also wirklich, wirklich anonyme Iteratorfunktionen möchten, verwenden Sie derzeit Visual Basic oder (ich habe es nicht überprüft) F #, wie in einem Kommentar von Teil # 7 in der Antwort von @Thomas Levesque angegeben (drücken Sie Strg + F für F #).

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.