Perché non è possibile visualizzare il rendimento della resa all'interno di un blocco di prova con una cattura?


95

Va bene quanto segue:

try
{
    Console.WriteLine("Before");

    yield return 1;

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

Il finallyblocco viene eseguito quando l'intera operazione è terminata ( IEnumerator<T>supporta IDisposableper fornire un modo per garantire ciò anche quando l'enumerazione viene abbandonata prima che termini).

Ma questo non va bene:

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

Supponiamo (per amor di discussione) che un'eccezione venga lanciata da una o dall'altra delle WriteLinechiamate all'interno del blocco try. Qual è il problema se si continua l'esecuzione in catchblocco?

Ovviamente, la parte yield return non è (attualmente) in grado di lanciare nulla, ma perché questo dovrebbe impedirci di avere un allegato try/ catchper gestire le eccezioni lanciate prima o dopo a yield return?

Aggiornamento: c'è un commento interessante di Eric Lippert qui - sembra che abbiano già abbastanza problemi nell'implementare correttamente il comportamento try / latest!

EDIT: la pagina MSDN su questo errore è: http://msdn.microsoft.com/en-us/library/cs1x15az.aspx . Tuttavia, non spiega perché.


2
Link diretto al commento di Eric Lippert: blogs.msdn.com/oldnewthing/archive/2008/08/14/…
Roman Starkov

nota: non puoi nemmeno cedere nel blocco catch stesso :-(
Simon_Weaver

2
Il collegamento oldnewthing non funziona più.
Sebastian Redl

Risposte:


50

Sospetto che sia una questione di praticità piuttosto che di fattibilità. Sospetto che ci siano pochissime volte in cui questa restrizione è effettivamente un problema che non può essere aggirato, ma la complessità aggiunta nel compilatore sarebbe molto significativa.

Ci sono alcune cose come questa che ho già riscontrato:

  • Attributi che non possono essere generici
  • Incapacità per X di derivare da XY (una classe annidata in X)
  • Iteratore blocca utilizzando campi pubblici nelle classi generate

In ognuno di questi casi sarebbe possibile guadagnare un po 'più di libertà, a costo di una maggiore complessità nel compilatore. Il team ha fatto la scelta pragmatica, per la quale li applaudo: preferirei un linguaggio leggermente più restrittivo con un compilatore accurato al 99,9% (sì, ci sono bug; ne ho incontrato uno su SO proprio l'altro giorno) piuttosto che un altro linguaggio flessibile che non può essere compilato correttamente.

EDIT: Ecco una pseudo-prova di come sia fattibile.

Considera che:

  • Puoi assicurarti che la parte yield return stessa non generi un'eccezione (calcola il valore in anticipo, quindi stai solo impostando un campo e restituendo "true")
  • Ti è permesso try / catch che non usa il rendimento del rendimento in un blocco iteratore.
  • Tutte le variabili locali nel blocco iteratore sono variabili di istanza nel tipo generato, quindi puoi spostare liberamente il codice su nuovi metodi

Ora trasforma:

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

into (sorta di pseudo-codice):

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

L'unica duplicazione è nell'impostazione di blocchi try / catch, ma è qualcosa che il compilatore può certamente fare.

Potrei essermi perso qualcosa qui - se è così, fammelo sapere!


11
Una buona prova di concetto, ma questa strategia diventa dolorosa (probabilmente più per un programmatore C # che per uno scrittore di compilatore C #) una volta che inizi a creare ambiti con cose come usinge foreach. Ad esempio:try{foreach (string s in c){yield return s;}}catch(Exception){}
Brian

La semantica normale di "try / catch" implica che se una qualsiasi parte di un blocco try / catch viene saltata a causa di un'eccezione, il controllo verrà trasferito a un blocco "catch" adatto, se presente. Sfortunatamente, se si verifica un'eccezione "durante" un rendimento di rendimento, non c'è modo per l'iteratore di distinguere i casi in cui viene eliminata a causa di un'eccezione da quelli in cui viene eliminata perché il proprietario ha recuperato tutti i dati di interesse.
supercat

7
"Sospetto che ci siano pochissime volte in cui questa restrizione è effettivamente un problema che non può essere aggirato" È un po 'come dire che non hai bisogno di eccezioni perché puoi usare la strategia di restituzione del codice di errore comunemente usata in C quindi molti anni fa. Ammetto che le difficoltà tecniche possano essere significative, ma questo limita ancora fortemente l'utilità yield, a mio parere, a causa del codice spaghetti che devi scrivere per aggirarlo.
jpmc26

@ jpmc26: No, non è affatto come dirlo. Non riesco a ricordare che questo mi abbia mai morso, e ho usato blocchi iteratori molte volte. E ' un po' limita l'utilità di yieldIMO - è una lunga strada da gravemente .
Jon Skeet

2
Questa 'caratteristica' in realtà richiede un po 'di codice piuttosto brutto, in alcuni casi per risolvere il problema, vedere stackoverflow.com/questions/5067188/...
namey

5

Tutte le yieldistruzioni in una definizione di iteratore vengono convertite in uno stato in una macchina a stati che utilizza effettivamente switchun'istruzione per far avanzare gli stati. Se ha fatto generare il codice per yieldle dichiarazioni in un try / catch avrebbe dovuto duplicare tutto in tryblocco per ogni yield dichiarazione, escludendo ogni altra yielddichiarazione per quel blocco. Questo non è sempre possibile, soprattutto se yieldun'affermazione dipende da una precedente.


2
Non credo di comprarlo. Penso che sarebbe del tutto fattibile, ma molto complicato.
Jon Skeet

2
I blocchi try / catch in C # non sono destinati a rientrare. Se li dividi, è possibile chiamare MoveNext () dopo un'eccezione e continuare il blocco try con uno stato possibilmente non valido.
Mark Cidade

2

Vorrei ipotizzare che a causa del modo in cui lo stack di chiamate viene ferito / sciolto quando si restituisce il ritorno da un enumeratore, diventa impossibile per un blocco try / catch effettivamente "catturare" l'eccezione. (perché il blocco di rendimento e ritorno non è in pila, anche se ha originato il blocco di iterazione)

Per avere un'idea di ciò di cui sto parlando, imposta un blocco iteratore e un foreach usando quell'iteratore. Controlla come appare lo Stack di chiamate all'interno del blocco foreach e poi controllalo all'interno dell'iteratore try / latest block.


Ho familiarità con lo svolgimento dello stack in C ++, in cui i distruttori vengono chiamati su oggetti locali che escono dall'ambito. La cosa corrispondente in C # sarebbe provare / finalmente. Ma quello svolgimento non si verifica quando si verifica il rendimento del rendimento. E per try / catch non è necessario che interagisca con il rendimento del rendimento.
Daniel Earwicker

Controlla cosa succede allo stack di chiamate durante il loop su un iteratore e capirai cosa intendo
Radu094

@ Radu094: No, sono sicuro che sarebbe possibile. Non dimenticare che gestisce già finalmente, il che è almeno in qualche modo simile.
Jon Skeet

2

Ho accettato la risposta di THE INVINCIBLE SKEET fino a quando qualcuno di Microsoft non arriva per versare acqua fredda sull'idea. Ma non sono d'accordo con la parte della questione dell'opinione - ovviamente un compilatore corretto è più importante di uno completo, ma il compilatore C # è già molto intelligente nel risolvere questa trasformazione per noi per quanto lo fa. Un po 'più di completezza in questo caso renderebbe la lingua più facile da usare, insegnare, spiegare, con meno casi limite o trucchi. Quindi penso che ne varrebbe la pena. Alcuni ragazzi a Redmond si grattano la testa per due settimane, e di conseguenza milioni di programmatori nel prossimo decennio possono rilassarsi un po 'di più.

(Nutro anche un sordido desiderio che ci sia un modo per yield returnlanciare un'eccezione che è stata inserita nella macchina a stati "dall'esterno", dal codice che guida l'iterazione. Ma le mie ragioni per volerlo sono piuttosto oscure.)

In realtà una domanda che ho sulla risposta di Jon ha a che fare con il lancio dell'espressione yield return.

Ovviamente il rendimento del rendimento 10 non è poi così male. Ma questo sarebbe un male:

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

Quindi non avrebbe più senso valutarlo all'interno del blocco try / catch precedente:

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;

Il prossimo problema sarebbe costituito dai blocchi try / catch annidati e dalle eccezioni rilanciate:

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

Ma sono sicuro che sia possibile ...


1
Sì, inseriresti la valutazione nella prova / cattura. Non importa dove metti l'impostazione della variabile. Il punto principale è che puoi effettivamente rompere una singola prova / cattura con un rendimento di rendimento in essa in due tentativi / catture con un rendimento di rendimento tra di loro.
Jon Skeet
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.