Warum kann die Rendite nicht in einem Try-Block mit einem Catch angezeigt werden?

95

Folgendes ist in Ordnung:

try
{
    Console.WriteLine("Before");

    yield return 1;

    Console.WriteLine("After");
}
finally
{
    Console.WriteLine("Done");
}

Der finallyBlock wird ausgeführt, wenn die Ausführung des gesamten Objekts abgeschlossen ist ( IEnumerator<T>unterstützt IDisposableeine Möglichkeit, dies sicherzustellen, auch wenn die Aufzählung vor Abschluss abgebrochen wird).

Das ist aber nicht in Ordnung:

try
{
    Console.WriteLine("Before");

    yield return 1;  // error CS1626: Cannot yield a value in the body of a try block with a catch clause

    Console.WriteLine("After");
}
catch (Exception e)
{
    Console.WriteLine(e.Message);
}

Angenommen, (aus Gründen der Argumentation) wird eine Ausnahme von dem einen oder anderen WriteLineAufruf im try-Block ausgelöst . Was ist das Problem bei der Fortsetzung der Ausführung im catchBlock?

Natürlich kann der Ertragsrendite-Teil (derzeit) nichts werfen, aber warum sollte uns das davon abhalten, ein Einschließen try/ catchAusnahmen zu behandeln, die vor oder nach einem geworfen werden yield return?

Update: Es gibt hier einen interessanten Kommentar von Eric Lippert - anscheinend haben sie bereits genug Probleme, das try / finally-Verhalten korrekt zu implementieren!

BEARBEITEN: Die MSDN-Seite zu diesem Fehler lautet: http://msdn.microsoft.com/en-us/library/cs1x15az.aspx . Es erklärt jedoch nicht warum.

Daniel Earwicker
quelle
2
Direkter Link zu Eric Lipperts Kommentar: blogs.msdn.com/oldnewthing/archive/2008/08/14/…
Roman
Hinweis: Sie können auch nicht im Catch-Block selbst nachgeben :-(
Simon_Weaver
2
Der Oldnewthing-Link funktioniert nicht mehr.
Sebastian Redl

Antworten:

50

Ich vermute, dass dies eher eine Frage der Praktikabilität als der Machbarkeit ist. Ich vermute, dass es sehr, sehr wenige Male gibt, in denen diese Einschränkung tatsächlich ein Problem ist, das nicht umgangen werden kann - aber die zusätzliche Komplexität im Compiler wäre sehr bedeutend.

Es gibt einige Dinge wie diese, die mir bereits begegnet sind:

  • Attribute können nicht generisch sein
  • Unfähigkeit von X, von XY abzuleiten (eine verschachtelte Klasse in X)
  • Iteratorblöcke mit öffentlichen Feldern in den generierten Klassen

In jedem dieser Fälle wäre es möglich, auf Kosten der zusätzlichen Komplexität des Compilers ein wenig mehr Freiheit zu gewinnen. Das Team traf die pragmatische Wahl, für die ich sie begrüße - ich hätte lieber eine etwas restriktivere Sprache mit einem zu 99,9% genauen Compiler (ja, es gibt Fehler; ich bin neulich auf SO gestoßen) als eine mehr flexible Sprache, die nicht richtig kompiliert werden konnte.

EDIT: Hier ist ein Pseudo-Beweis dafür, warum es machbar ist.

Berücksichtige das:

  • Sie können sicherstellen, dass der Yield Return-Teil selbst keine Ausnahme auslöst (berechnen Sie den Wert vorab, und setzen Sie dann einfach ein Feld und geben "true" zurück).
  • Du darfst try / catch, das in einem Iteratorblock keine Yield Return verwendet.
  • Alle lokalen Variablen im Iteratorblock sind Instanzvariablen im generierten Typ, sodass Sie Code frei in neue Methoden verschieben können

Nun transformiere:

try
{
    Console.WriteLine("a");
    yield return 10;
    Console.WriteLine("b");
}
catch (Something e)
{
    Console.WriteLine("Catch block");
}
Console.WriteLine("Post");

in (Art von Pseudocode):

case just_before_try_state:
    try
    {
        Console.WriteLine("a");
    }
    catch (Something e)
    {
        CatchBlock();
        goto case post;
    }
    __current = 10;
    return true;

case just_after_yield_return:
    try
    {
        Console.WriteLine("b");
    }
    catch (Something e)
    {
        CatchBlock();
    }
    goto case post;

case post;
    Console.WriteLine("Post");


void CatchBlock()
{
    Console.WriteLine("Catch block");
}

Die einzige Überschneidung besteht darin, Try / Catch-Blöcke einzurichten - aber das kann der Compiler auf jeden Fall.

Vielleicht habe ich hier etwas verpasst - wenn ja, lassen Sie es mich bitte wissen!

Jon Skeet
quelle
11
Ein guter Proof-of-Concept, aber diese Strategie wird schmerzhaft (wahrscheinlich mehr für einen C # -Programmierer als für einen C # -Compiler-Writer), sobald Sie anfangen, Bereiche mit Dingen wie usingund zu erstellen foreach. Zum Beispiel:try{foreach (string s in c){yield return s;}}catch(Exception){}
Brian
Die normale Semantik von "try / catch" impliziert, dass, wenn ein Teil eines try / catch-Blocks aufgrund einer Ausnahme übersprungen wird, die Steuerung auf einen geeigneten "catch" -Block übertragen wird, falls einer vorhanden ist. Wenn eine Ausnahme "während" einer Ertragsrendite auftritt, kann der Iterator die Fälle, in denen sie aufgrund einer Ausnahme entsorgt wird, leider nicht von den Fällen unterscheiden, in denen sie entsorgt wird, weil der Eigentümer alle interessierenden Daten abgerufen hat.
Supercat
7
"Ich vermute, dass es sehr, sehr wenige Male gibt, in denen diese Einschränkung tatsächlich ein Problem ist, das nicht umgangen werden kann." vor vielen Jahren. Ich gebe zu, dass die technischen Schwierigkeiten erheblich sein können, aber dies schränkt die Nützlichkeit yieldmeiner Meinung nach aufgrund des Spaghetti-Codes, den Sie schreiben müssen, um ihn zu umgehen, immer noch stark ein .
jpmc26
@ jpmc26: Nein, das ist wirklich gar nicht so. Ich kann mich nicht erinnern , dass mich das jemals gebissen hat, und ich habe oft Iteratorblöcke verwendet. Es schränkt die Nützlichkeit von IMO leicht einyield - es ist weit davon entfernt, ernsthaft zu sein .
Jon Skeet
2
Diese 'Funktion' erfordert in einigen Fällen einen ziemlich hässlichen Code, um das Problem zu
umgehen.
5

Alle yieldAnweisungen in einer Iteratordefinition werden in einen Zustand in einer Zustandsmaschine konvertiert, die eine switchAnweisung effektiv verwendet , um Zustände voranzutreiben. Wenn es tut generieren Code für yieldAnweisungen in einem try / catch müßte es kopieren alles in dem tryfür den Block jeder yield Aussage , während jeden zweiter ohne yieldErklärung für diesen Block. Dies ist nicht immer möglich, insbesondere wenn eine yieldAnweisung von einer früheren abhängig ist.

Mark Cidade
quelle
2
Ich glaube nicht, dass ich das kaufe. Ich denke, es wäre durchaus machbar - aber sehr kompliziert.
Jon Skeet
2
Try / Catch-Blöcke in C # sind nicht für den Wiedereintritt gedacht. Wenn Sie sie aufteilen, können Sie MoveNext () nach einer Ausnahme aufrufen und den try-Block mit einem möglicherweise ungültigen Status fortsetzen.
Mark Cidade
2

Ich würde spekulieren, dass es aufgrund der Art und Weise, wie der Aufrufstapel verwundet / abgewickelt wird, wenn Sie die Rendite eines Enumerators erzielen, für einen try / catch-Block unmöglich wird, die Ausnahme tatsächlich zu "fangen". (weil sich der Yield Return Block nicht auf dem Stack befindet, obwohl er den Iterationsblock erstellt hat)

Um eine Vorstellung davon zu bekommen, wovon ich spreche, richten Sie einen Iteratorblock und einen Foreach mit diesem Iterator ein. Überprüfen Sie, wie der Call Stack im foreach-Block aussieht, und überprüfen Sie ihn dann im iterator try / finally-Block.

Radu094
quelle
Ich bin mit dem Abwickeln von Stapeln in C ++ vertraut, wo Destruktoren für lokale Objekte aufgerufen werden, die außerhalb des Gültigkeitsbereichs liegen. Das entsprechende Ding in C # wäre try / finally. Diese Abwicklung tritt jedoch nicht auf, wenn eine Rendite erzielt wird. Und für Try / Catch ist es nicht erforderlich, mit der Rendite zu interagieren.
Daniel Earwicker
Überprüfen Sie, was mit dem Aufrufstapel passiert, wenn Sie einen Iterator
durchlaufen,
@ Radu094: Nein, ich bin sicher, dass es möglich wäre. Vergiss nicht, dass es schon endlich geht, was zumindest etwas ähnlich ist.
Jon Skeet
2

Ich habe die Antwort von THE INVINCIBLE SKEET akzeptiert, bis jemand von Microsoft kommt, um kaltes Wasser auf die Idee zu gießen. Aber ich bin mit dem Teil der Meinungssache nicht einverstanden - natürlich ist ein korrekter Compiler wichtiger als ein vollständiger, aber der C # -Compiler ist bereits sehr geschickt darin, diese Transformation für uns zu klären, soweit dies der Fall ist. Ein wenig mehr Vollständigkeit in diesem Fall würde es einfacher machen, die Sprache zu verwenden, zu unterrichten, zu erklären, mit weniger Randfällen oder Fallstricken. Ich denke, es wäre die zusätzliche Mühe wert. Ein paar Leute in Redmond kratzen sich vierzehn Tage lang am Kopf, und infolgedessen können sich Millionen von Programmierern im nächsten Jahrzehnt etwas mehr entspannen.

(Ich habe auch den schmutzigen Wunsch, dass es eine Möglichkeit gibt yield return, eine Ausnahme zu machen , die "von außen" durch den Code, der die Iteration steuert, in die Zustandsmaschine gestopft wurde. Aber meine Gründe, dies zu wollen, sind ziemlich dunkel.)

Eigentlich hat eine Frage, die ich zu Jons Antwort habe, mit dem Auslösen des Yield Return-Ausdrucks zu tun.

Offensichtlich ist die Rendite 10 nicht so schlecht. Aber das wäre schlecht:

yield return File.ReadAllText("c:\\missing.txt").Length;

Wäre es also nicht sinnvoller, dies im vorherigen Try / Catch-Block zu bewerten:

case just_before_try_state:
    try
    {
        Console.WriteLine("a");
        __current = File.ReadAllText("c:\\missing.txt").Length;
    }
    catch (Something e)
    {
        CatchBlock();
        goto case post;
    }
    return true;

Das nächste Problem wären verschachtelte Try / Catch-Blöcke und erneut geworfene Ausnahmen:

try
{
    Console.WriteLine("x");

    try
    {
        Console.WriteLine("a");
        yield return 10;
        Console.WriteLine("b");
    }
    catch (Something e)
    {
        Console.WriteLine("y");

        if ((DateTime.Now.Second % 2) == 0)
            throw;
    }
}
catch (Something e)
{
    Console.WriteLine("Catch block");
}
Console.WriteLine("Post");

Aber ich bin sicher, es ist möglich ...

Daniel Earwicker
quelle
1
Ja, Sie würden die Bewertung in den Versuch / Fang einfügen. Es spielt keine Rolle, wo Sie die Variableneinstellung platzieren. Der Hauptpunkt ist, dass Sie einen einzelnen Versuch / Fang mit einer Ertragsrendite effektiv in zwei Versuche / Fänge mit einer Ertragsrendite zwischen ihnen aufteilen können.
Jon Skeet