Perché un'istruzione "continue" non può trovarsi all'interno di un blocco "finalmente"?


107

Non ho problemi; Sono solo curioso. Immagina il seguente scenario:

foreach (var foo in list)
{
    try
    {
         //Some code
    }
    catch (Exception)
    {
        //Some more code
    }
    finally
    {
        continue;
    }
}

Questo non verrà compilato, poiché genera l' errore del compilatore CS0157 :

Il controllo non può lasciare il corpo di una clausola finalmente

Perché?


7
Così. Solo curioso. Se per te ha perfettamente senso il motivo per cui non si compila, perché vuoi che qualcuno ti spieghi cosa ha già senso? =)
J. Steen

14
motivo per cui si avrebbe bisogno di un continue;in finallyblocco? non è lo stesso che continue;dopo il try -- catchblocco?
bansi

6
@ J.Steen No, lo so che finally/continueè una limitazione del compilatore C # :-) Anch'io sono curioso del motivo di questa limitazione.
xanatos

5
@xanatos - Tecnicamente parlando, è una limitazione del CIL sottostante. Dalle specifiche della lingua : "Il trasferimento del controllo non è mai consentito per entrare in un gestore catch o in una clausola finalmente se non attraverso il meccanismo di gestione delle eccezioni." e "Il trasferimento del controllo da una regione protetta è consentito solo tramite un'istruzione di eccezione (leave, end.filter, end.catch o end.finally)." La brfamiglia delle istruzioni di ramo non può farlo.
Non firmato il

1
@ Unsigned: è anche una buona limitazione,
ammetterlo

Risposte:


150

finallyi blocchi vengono eseguiti indipendentemente dal fatto che venga generata un'eccezione. Se viene generata un'eccezione, cosa diavolo farebbe continue? Non è possibile continuare l'esecuzione del ciclo, perché un'eccezione non rilevata trasferirà il controllo a un'altra funzione.

Anche se non viene generata alcuna eccezione, finallyverrà eseguita quando vengono eseguite altre istruzioni di trasferimento del controllo all'interno del blocco try / catch return, ad esempio un, che porta lo stesso problema.

In breve, con la semantica di finallyesso non ha senso consentire il trasferimento del controllo dall'interno di un finallyblocco all'esterno di esso.

Supportare questo con alcune semantiche alternative sarebbe più confuso che utile, poiché ci sono semplici soluzioni alternative che rendono il comportamento previsto molto più chiaro. Quindi ottieni un errore e sei costretto a pensare correttamente al tuo problema. È l'idea generale "gettarti nella fossa del successo" che va avanti in C #.

C #, tu e l'uscita in caso di successo

Se vuoi ignorare le eccezioni (il più delle volte è una cattiva idea) e continuare a eseguire il ciclo, usa un blocco catch all:

foreach ( var in list )
{
    try{
        //some code
    }catch{
        continue;
    }
}

Se vuoi farlo continuesolo quando non vengono lanciate eccezioni non rilevate, mettilo continuefuori dal try-block.


12
Accetterò questa come risposta, perché tu fai capire i motivi per cui Microsoft ha eventualmente deciso di non accettare una continuazione in modo definitivo. Probabilmente quelle immagini hanno convinto anche me :)
lpaloub

20
puoi elaborare l'idea del "gettarti nella fossa del successo"? Non ho capito :-D
Ant


13
L'immagine è stata fatta da Jon Skeet, btw. Ho ricevuto da qui: msmvps.com/blogs/jon_skeet/archive/2010/09/02/…
R. Martinho Fernandes

1
Naturalmente, un continuein un finallyblocco andrà bene se continua solo un loop locale definito all'interno del finallyblocco. Nella domanda, tuttavia, cerca di continuare un ciclo "esterno". Dichiarazioni simili che non puoi avere all'interno di un finallyblocco sono return, break(quando esci dal blocco) e goto(quando vai a un'etichetta fuori dal finallyblocco). Per una discussione su Java correlata, vedere Ritorno da un blocco finalmente in Java .
Jeppe Stig Nielsen

32

Ecco una fonte affidabile:

Un'istruzione continue non può uscire da un blocco finally (Sezione 8.10). Quando un'istruzione continue si verifica all'interno di un blocco finalmente, la destinazione dell'istruzione continue deve trovarsi all'interno dello stesso blocco finalmente; in caso contrario, si verifica un errore in fase di compilazione.

È tratto da MSDN, 8.9.2 L'istruzione continue .

La documentazione dice che:

Le istruzioni di un blocco finalmente vengono sempre eseguite quando il controllo lascia un'istruzione try. Questo è vero se il trasferimento del controllo si verifica come risultato della normale esecuzione, come risultato dell'esecuzione di un'istruzione break, continue, goto o return, o come risultato della propagazione di un'eccezione dall'istruzione try. Se viene generata un'eccezione durante l'esecuzione di un blocco latest, l'eccezione viene propagata alla successiva istruzione try racchiusa. Se un'altra eccezione era in fase di propagazione, quell'eccezione viene persa. Il processo di propagazione di un'eccezione è discusso ulteriormente nella descrizione dell'istruzione throw (Sezione 8.9.5).

È da qui 8.10 L'istruzione try .


31

Potresti pensare che abbia senso, ma in realtà non ha senso .

foreach (var v in List)
{
    try
    {
        //Some code
    }
    catch (Exception)
    {
        //Some more code
        break; or return;
    }
    finally
    {
        continue;
    }
}

Cosa intendi fare una pausa o un continue quando viene generata un'eccezione? Il team del compilatore C # non vuole prendere decisioni da solo assumendo breako continue. Invece, hanno deciso di lamentarsi del fatto che la situazione degli sviluppatori sarà ambigua da cui trasferire il controllo finally block.

Quindi è compito dello sviluppatore affermare chiaramente cosa intende fare piuttosto che il compilatore presumere qualcos'altro.

Spero che tu capisca perché questo non si compila!


In una nota correlata, anche se non ci fosse "cattura", il percorso di esecuzione che segue finallyun'istruzione sarebbe influenzato dal fatto che catchsia uscito tramite eccezione e non esiste un meccanismo per dire come dovrebbe interagire con a continue. Mi piace il tuo esempio, però, perché mostra un problema ancora più grande.
supercat

@supercat D'accordo con te, la mia risposta mostra un esempio di situazione ambigua per il compilatore, ma ci sono così tanti problemi con questo approccio.
Sriram Sakthivel

16

Come altri hanno affermato, ma incentrato sulle eccezioni, si tratta in realtà di una gestione ambigua del trasferimento del controllo.

Nella tua mente, probabilmente stai pensando a uno scenario come questo:

public static object SafeMethod()
{
    foreach(var item in list)
    {
        try
        {
            try
            {
                //do something that won't transfer control outside
            }
            catch
            {
                //catch everything to not throw exceptions
            }
        }
        finally
        {
            if (someCondition)
                //no exception will be thrown, 
                //so theoretically this could work
                continue;
        }
    }

    return someValue;
}

Teoricamente, puoi monitorare il flusso di controllo e dire, sì, questo è "ok". Non viene generata alcuna eccezione, nessun controllo viene trasferito. Ma i progettisti del linguaggio C # avevano in mente altri problemi.

L'eccezione lanciata

public static void Exception()
{
    try
    {
        foreach(var item in list)
        {
            try
            {
                throw new Exception("What now?");
            }
            finally
            {
                continue;
            }
        }
    }
    catch
    {
        //do I get hit?
    }
}

Il temuto Goto

public static void Goto()
{
    foreach(var item in list)
    {
        try
        {
            goto pigsfly;
        }
        finally
        {
            continue;
        }
    }

    pigsfly:
}

Il ritorno

public static object ReturnSomething()
{
    foreach(var item in list)
    {
        try
        {
            return item;
        }
        finally
        {
            continue;
        }
    }
}

La separazione

public static void Break()
{
    foreach(var item in list)
    {
        try
        {
            break;
        }
        finally
        {
            continue;
        }
    }
}

Quindi, in conclusione, sì, sebbene vi sia una leggera possibilità di utilizzare a continuein situazioni in cui il controllo non viene trasferito, ma una buona parte (la maggioranza?) Dei casi comporta eccezioni o returnblocchi. I progettisti del linguaggio hanno ritenuto che ciò sarebbe stato troppo ambiguo e (probabilmente) impossibile per garantire al momento della compilazione che il tuo continuesia utilizzato solo nei casi in cui il flusso di controllo non viene trasferito.


Ho avuto la stessa situazione in java quando proguard si bloccava durante l'offuscamento. La tua spiegazione è abbastanza buona. C # fornisce un errore in fase di compilazione: ha perfettamente senso.
Dev_Vikram

11

In generale continuenon ha senso se usato in finallyblocco. Guarda questo:

foreach (var item in list)
{
    try
    {
        throw new Exception();
    }
    finally{
        //doesn't make sense as we are after exception
        continue;
    }
}

"In generale" molte cose non hanno senso. E non significa che non dovrebbe funzionare "in particolare". IE: continuenon ha senso al di fuori dell'istruzione loop, ma non significa che non sia supportata.
zerkms

Penso che abbia un senso. itempuò essere file, lettura fallita -> finallychiude il file. continueimpedisce il resto dell'elaborazione.
jnovacho

5

"Questo non si compila e penso che abbia perfettamente senso"

Beh, penso di no.

Quando hai letteralmente catch(Exception), non hai bisogno di finalmente (e probabilmente nemmeno di continue).

Quando hai il più realistico catch(SomeException), cosa dovrebbe accadere se un'eccezione non viene rilevata? Il tuo continuevuole andare in un modo, l'eccezione ne gestisce un altro.


2
Penso che potresti aver bisogno finallyquando lo hai catch. È comunemente usato per chiudere le risorse in modo affidabile.
jnovacho

Non sono solo eccezioni però. Potresti facilmente avere una returndichiarazione all'interno dei tuoi blocchi tryo catch. Cosa facciamo adesso? Tornare o continuare il ciclo? (e se continuiamo, cosa succede se è l'ultimo elemento da iterare? Quindi continuiamo fuori foreache il ritorno non avviene mai?) EDIT: O anche gotoa un'etichetta fuori dal ciclo, praticamente qualsiasi azione che trasferisce il controllo al di fuori del foreachciclo .
Chris Sinclair

2
Non sono d'accordo con "Quando hai letteralmente cattura (eccezione), allora non hai bisogno di finalmente (e probabilmente nemmeno di continuare).". E se avessi bisogno di eseguire un'operazione qualunque sia stata un'eccezione o meno?
lpaloub

OK, finalmente si occupa di ulteriori messaggi returnall'interno del blocco. Ma normalmente l'esecuzione continua dopo il catch-all.
Henk Holterman

"finalmente" non è "cattura". Dovrebbe essere usato per il codice di pulizia. un blocco finalmente viene eseguito indipendentemente dal fatto che venga generata un'eccezione o meno . Cose come chiudere i file o liberare memoria (se stai usando codice non gestito) ecc. Queste sono le cose che metti in un blocco finalmente
Robotnik

3

Non puoi lasciare il corpo di un blocco finale. Ciò include break, return e nel tuo caso continue keywords.


3

Il finallyblocco può essere eseguito con un'eccezione in attesa di essere rilanciata. Non avrebbe davvero senso essere in grado di uscire dal blocco (da a continueo qualsiasi altra cosa) senza rilanciare l'eccezione.

Se vuoi continuare il tuo ciclo qualunque cosa accada, non hai bisogno dell'istruzione infine: prendi semplicemente l'eccezione e non rilanciare.


1

finallyviene eseguito indipendentemente dal fatto che venga generata un'eccezione non rilevata. Altri hanno già spiegato perché questo rende continueillogico, ma ecco un'alternativa che segue lo spirito di ciò che questo codice sembra chiedere. Fondamentalmente, finally { continue; }sta dicendo:

  1. Quando vengono rilevate eccezioni, continuare
  2. Quando ci sono eccezioni non rilevate, consenti che vengano lanciate, ma continua comunque

(1) potrebbe essere soddisfatta inserendo continuealla fine di ciascuna catch, e (2) potrebbe essere soddisfatta memorizzando eccezioni non rilevate da lanciare in seguito. Potresti scriverlo così:

var exceptions = new List<Exception>();
foreach (var foo in list) {
    try {
        // some code
    } catch (InvalidOperationException ex) {
        // handle specific exception
        continue;
    } catch (Exception ex) {
        exceptions.Add(ex);
        continue;
    }
    // some more code
}
if (exceptions.Any()) {
    throw new AggregateException(exceptions);
}

In realtà, finallysarebbe stato giustiziato anche nel terzo caso, dove non c'erano eccezioni lanciate, catturate o non prese. Se lo desideri, puoi ovviamente posizionare un singolo continuedopo il blocco try-catch invece che all'interno di ciascuno catch.


1

Tecnicamente parlando, è una limitazione del CIL sottostante. Dalle specifiche della lingua :

Il trasferimento del controllo non è mai consentito per entrare in una clausola catch handler o finalmente se non attraverso il meccanismo di gestione delle eccezioni.

e

Trasferire il controllo di una regione protetta è consentito solo attraverso un'istruzione un'eccezione ( leave, end.filter, end.catch, o end.finally)

Nella pagina del documento per le bristruzioni :

I trasferimenti di controllo dentro e fuori dai blocchi try, catch, filter e infine non possono essere eseguiti da questa istruzione.

Quest'ultima vale per tutte le istruzioni filiali, tra cui beq, brfalse, etc.


-1

I progettisti del linguaggio semplicemente non volevano (o non potevano) ragionare sulla semantica di un blocco finalmente terminato da un trasferimento di controllo.

Un problema, o forse il problema chiave, è che il finallyblocco viene eseguito come parte di un trasferimento di controllo non locale (elaborazione delle eccezioni). L'obiettivo di quel trasferimento di controllo non è il ciclo che lo racchiude; l'elaborazione dell'eccezione interrompe il ciclo e continua lo svolgimento ulteriormente.

Se abbiamo un trasferimento di controllo fuori dal finallyblocco di pulizia, il trasferimento di controllo originale viene "dirottato". Viene annullato e il controllo va altrove.

La semantica può essere elaborata. Altre lingue ce l'hanno.

I progettisti di C # hanno deciso di disabilitare semplicemente i trasferimenti di controllo statici "goto-like", semplificando in tal modo le cose.

Tuttavia, anche se lo fai, non risolve la questione di cosa succede se un trasferimento dinamico viene avviato da a finally: cosa succede se il blocco finalmente chiama una funzione e quella funzione lancia? L'elaborazione dell'eccezione originale viene quindi "dirottata".

Se risolvi la semantica di questa seconda forma di dirottamento, non c'è motivo di bandire il primo tipo. Sono davvero la stessa cosa: un trasferimento di controllo è un trasferimento di controllo, che si tratti dello stesso ambito lessicale o meno.


La differenza è che finallyper definizione deve essere immediatamente seguito dalla continuazione del trasferimento che lo ha attivato (eccezione non rilevata return, ecc.). Sarebbe facile consentire trasferimenti "goto-like" all'interno finally(quali lingue lo fanno, a proposito?), Ma farlo significherebbe che try { return } finally { ... } potrebbe non tornare , il che è completamente inaspettato. Se finallychiama una funzione che genera, non è proprio la stessa cosa, perché già ci aspettiamo che le eccezioni possano verificarsi in qualsiasi momento e interrompere il flusso normale.
nmclean

@nmclean: In realtà, un'eccezione che sfugge finallyè più o meno la stessa cosa, in quanto può provocare un flusso di programma inaspettato e illogico. L'unica differenza è che i progettisti del linguaggio, non essendo in grado di rifiutare tutti i programmi in cui un blocco finalmente potrebbe generare un'eccezione non gestita, consentono invece a tali programmi di compilarsi e sperano che il programma possa accettare qualsiasi conseguenza che potrebbe seguire l'abbandono del precedente sblocco delle eccezioni sequenza.
supercat

@supercat Sono d'accordo che interrompe il flusso previsto, ma il mio punto è che questo è sempre il caso delle eccezioni non gestite, indipendentemente dal fatto che il flusso corrente sia finallyo meno. Secondo la logica dell'ultimo paragrafo di Kaz, le funzioni dovrebbero anche essere libere di influenzare il flusso nell'ambito del loro chiamante tramite continueecc., Perché sono autorizzate a farlo in via di eccezioni. Ma questo ovviamente non è permesso; le eccezioni sono l' unica eccezione a questa regola, da cui il nome "eccezione" (o "interruzione").
nmclean

@nmclean: le eccezioni non annidate rappresentano un flusso di controllo che differisce dalla normale esecuzione, ma è comunque strutturato. L'istruzione che segue un blocco dovrebbe essere eseguita solo se tutte le eccezioni che si sono verificate all'interno di quel blocco sono state intercettate al suo interno. Potrebbe sembrare un'aspettativa ragionevole, ma un'eccezione che si verifica all'interno di un finallyblocco può violarla. Davvero una brutta situazione, che IMHO avrebbe dovuto essere risolta almeno lasciando che i finallyblocchi venissero a conoscenza di eventuali eccezioni in sospeso in modo che possano costruire oggetti eccezione compositi.
supercat

@mmclean Mi dispiace, non sono d'accordo che il nome "eccezione" derivi da qualche "eccezione alla regola" (relativa al controllo) specifica di C #, LOL. L'aspetto che altera il flusso di controllo di un'eccezione è semplicemente un trasferimento di controllo dinamico non locale. Un regolare trasferimento di controllo all'interno dello stesso ambito lessicale (senza che abbia luogo uno svolgimento) può essere considerato un caso speciale di questo.
Kaz il
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.