Dlaczego zwrot „yield” nie może pojawić się wewnątrz bloku try z haczykiem?

97

Poniższe są w porządku:

try
{
    Console.WriteLine("Before");

    yield return 1;

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

finallyBlok pracuje, gdy cała sprawa zakończyła wykonywanie ( IEnumerator<T>podpory IDisposable, aby zapewnić sposób, aby zapewnić to nawet wtedy, gdy wyliczenie zostanie przerwany przed jej zakończeniem).

Ale to nie jest w porządku:

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

Załóżmy (ze względu na argument), że jedno lub drugie WriteLinewywołanie wewnątrz bloku try zgłasza wyjątek . Jaki jest problem z kontynuowaniem wykonywania w catchbloku?

Oczywiście, część zwrotu z zysku nie jest (obecnie) w stanie niczego rzucić, ale dlaczego to powinno nas powstrzymywać od dołączania try/ catchzajmowania się wyjątkami rzucanymi przed lub po yield return?

Aktualizacja: Jest tu ciekawy komentarz od Erica Lipperta - wydaje się, że mają już wystarczająco dużo problemów z poprawnym wdrożeniem zachowania try / final!

EDYTUJ: Strona MSDN dotycząca tego błędu to: http://msdn.microsoft.com/en-us/library/cs1x15az.aspx . Nie wyjaśnia to jednak, dlaczego.

Daniel Earwicker
źródło
2
Bezpośredni link do komentarza Erica Lipperta: blogs.msdn.com/oldnewthing/archive/2008/08/14/…
Roman Starkov
uwaga: nie możesz też ustąpić w samym bloku catch :-(
Simon_Weaver
2
Link Oldnewthing już nie działa.
Sebastian Redl

Odpowiedzi:

50

Podejrzewam, że jest to kwestia praktyczności, a nie wykonalności. Podejrzewam, że są bardzo, bardzo kilka razy, gdzie ograniczenie to rzeczywiście problem, że nie można obejść - ale dodał złożoność kompilator byłoby bardzo istotne.

Jest kilka rzeczy, które już napotkałem:

  • Atrybuty nie mogą być ogólne
  • Niezdolność X do wyprowadzenia z XY (klasa zagnieżdżona w X)
  • Bloki iteracyjne używające pól publicznych w wygenerowanych klasach

W każdym z tych przypadków możliwe byłoby uzyskanie nieco większej swobody kosztem dodatkowej złożoności kompilatora. Zespół dokonał pragmatycznego wyboru, za co im oklaskiwam - wolałbym mieć nieco bardziej restrykcyjny język z kompilatorem dokładnym w 99,9% (tak, są błędy; natknąłem się na jeden z SO dopiero niedawno) niż bardziej elastyczny język, który nie mógł się poprawnie skompilować.

EDYCJA: Oto pseudo-dowód na to, dlaczego jest to wykonalne.

Weź pod uwagę, że:

  • Możesz upewnić się, że sama część zwracająca wydajność nie zgłasza wyjątku (wstępnie oblicz wartość, a następnie ustawiasz tylko pole i zwracasz „prawda”)
  • Możesz spróbować / catch, który nie używa zwrotu zysku w bloku iteratora.
  • Wszystkie zmienne lokalne w bloku iteratora są zmiennymi instancji w wygenerowanym typie, więc możesz swobodnie przenosić kod do nowych metod

Teraz przekształć:

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

into (rodzaj pseudokodu):

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

Jedyne powielanie polega na ustawianiu bloków try / catch - ale kompilator z pewnością może to zrobić.

Mogłem coś tutaj przeoczyć - jeśli tak, daj mi znać!

Jon Skeet
źródło
11
Dobry dowód koncepcji, ale ta strategia staje się bolesna (prawdopodobnie bardziej dla programisty C # niż dla autora kompilatora C #), gdy zaczniesz tworzyć zakresy z takimi elementami jak usingi foreach. Na przykład:try{foreach (string s in c){yield return s;}}catch(Exception){}
Brian
Normalna semantyka „try / catch” oznacza, że ​​jeśli jakakolwiek część bloku try / catch zostanie pominięta z powodu wyjątku, sterowanie zostanie przeniesione do odpowiedniego bloku „catch”, jeśli taki istnieje. Niestety, jeśli wyjątek wystąpi „podczas” zwrotu zysku, iterator nie może odróżnić przypadków, w których jest usuwany z powodu wyjątku od tych, w których jest usuwany, ponieważ właściciel pobrał wszystkie interesujące dane.
supercat
7
„Podejrzewam, że bardzo, bardzo często zdarza się, że to ograniczenie jest problemem, którego nie można obejść”. wiele lat temu. Przyznaję, że trudności techniczne mogą być znaczące, ale to nadal poważnie ogranicza użyteczność yield, moim zdaniem, ze względu na kod spaghetti, który trzeba napisać, aby go obejść.
jpmc26
@ jpmc26: Nie, naprawdę wcale tak nie jest. Nie pamiętam, żeby kiedykolwiek mnie to gryzło i wiele razy używałem bloków iteratora. Ogranicza to nieco użyteczność yieldIMO - daleko do poważnych .
Jon Skeet
2
Ta
``
5

Wszystkie yieldinstrukcje w definicji iteratora są konwertowane na stan w automacie stanowym, który efektywnie używa switchinstrukcji do przechodzenia do kolejnych stanów. Jeśli to nie generuje kod dla yieldinstrukcji w try / catch musiałby powielać wszystko w trybloku dla każdego yield rachunku z wykluczeniem wszelkich innych yieldinstrukcji dla tego bloku. Nie zawsze jest to możliwe, szczególnie jeśli jedna yieldinstrukcja jest zależna od wcześniejszej.

Mark Cidade
źródło
2
Chyba tego nie kupuję. Myślę, że byłoby to całkowicie wykonalne - ale bardzo skomplikowane.
Jon Skeet,
2
Bloki try / catch w języku C # nie są przeznaczone do ponownego wejścia. Jeśli je podzielisz, możliwe jest wywołanie MoveNext () po wyjątku i kontynuowanie bloku try z prawdopodobnie nieprawidłowym stanem.
Mark Cidade
2

Spekulowałbym, że ze względu na sposób, w jaki stos wywołań jest nawijany / rozwijany, gdy zwracasz wynik z modułu wyliczającego, blok try / catch nie może faktycznie „złapać” wyjątku. (ponieważ bloku zwrotu zysku nie ma na stosie, mimo że zapoczątkował blok iteracji)

Aby uzyskać wyobrażenie o tym, o czym mówię, skonfiguruj blok iteratora i foreach używający tego iteratora. Sprawdź, jak wygląda stos wywołań wewnątrz bloku foreach, a następnie sprawdź go w iteratorze try / final block.

Radu094
źródło
Jestem zaznajomiony z rozwijaniem stosu w C ++, gdzie destruktory są wywoływane na lokalnych obiektach, które wykraczają poza zakres. Odpowiednią rzeczą w C # byłoby try / w końcu. Ale to odprężenie nie następuje, gdy następuje zwrot plonów. W przypadku try / catch nie ma potrzeby interakcji ze zwrotem zysku.
Daniel Earwicker,
Sprawdź, co dzieje się ze stosem wywołań podczas pętli nad iteratorem, a zrozumiesz, o co mi chodzi
Radu094,
@ Radu094: Nie, jestem pewien, że byłoby to możliwe. Nie zapominaj, że w końcu już sobie radzi, co jest przynajmniej trochę podobne.
Jon Skeet
2

Zaakceptowałem odpowiedź THE INVINCIBLE SKEET, dopóki nie pojawi się ktoś z Microsoftu, by oblać ten pomysł zimną wodą. Ale nie zgadzam się z częścią opiniodawczą - oczywiście poprawny kompilator jest ważniejszy niż kompletny, ale kompilator C # jest już bardzo sprytny, jeśli chodzi o uporządkowanie tej transformacji dla nas, o ile to robi. Nieco większa kompletność w tym przypadku ułatwiłaby używanie, nauczanie, wyjaśnianie języka, z mniejszą liczbą skrajnych przypadków lub problemów. Więc myślę, że byłoby to warte dodatkowego wysiłku. Kilku facetów z Redmond drapie się po głowach przez dwa tygodnie, w wyniku czego miliony programistów w ciągu następnej dekady mogą się trochę bardziej zrelaksować.

(Mam również podłe pragnienie, aby istniał sposób na zrobienie yield returnwyjątku rzutowania, który został wepchnięty do automatu stanowego „z zewnątrz” przez kod sterujący iteracją. Ale powody, dla których chcę tego, są dość niejasne).

Właściwie jedno pytanie, które mam na temat odpowiedzi Jona, dotyczy wyrzucania wyrażenia zwrotu zysku.

Oczywiście zwrot z zysku 10 nie jest taki zły. Ale to byłoby złe:

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

Czy więc nie miałoby sensu oceniać tego w poprzedzającym bloku try / catch:

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;

Następnym problemem byłyby zagnieżdżone bloki try / catch i wyjątki ponownego odrzucenia:

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");

Ale jestem pewien, że to możliwe ...

Daniel Earwicker
źródło
1
Tak, umieściłbyś ocenę w próbie / złapaniu. Tak naprawdę nie ma znaczenia, gdzie umieścisz ustawienie zmiennej. Najważniejsze jest to, że możesz skutecznie przełamać pojedynczy try / catch ze zwrotem zysku na dwa try / catch ze zwrotem zysku między nimi.
Jon Skeet,