Perché le variabili non sono dichiarate in "prova" nell'ambito "in" cattura "o" finalmente "?


139

In C # e Java (e forse anche in altre lingue), le variabili dichiarate in un blocco "try" non rientrano nell'ambito dei blocchi "catch" o "finally" corrispondenti. Ad esempio, il seguente codice non viene compilato:

try {
  String s = "test";
  // (more code...)
}
catch {
  Console.Out.WriteLine(s);  //Java fans: think "System.out.println" here instead
}

In questo codice, si verifica un errore di compilazione nel riferimento a s nel blocco catch, poiché s è solo nell'ambito nel blocco try. (In Java, l'errore di compilazione è "impossibile risolvere"; in C # è "Il nome 's' non esiste nel contesto corrente".)

La soluzione generale a questo problema sembra essere invece dichiarare le variabili appena prima del blocco try, anziché all'interno del blocco try:

String s;
try {
  s = "test";
  // (more code...)
}
catch {
  Console.Out.WriteLine(s);  //Java fans: think "System.out.println" here instead
}

Tuttavia, almeno per me, (1) sembra una soluzione ingombrante, e (2) risulta che le variabili abbiano un ambito più ampio di quello previsto dal programmatore (l'intero resto del metodo, anziché solo nel contesto del try-catch-finally).

La mia domanda è: quali sono / sono le motivazioni alla base di questa decisione di progettazione del linguaggio (in Java, in C # e / o in qualsiasi altra lingua applicabile)?

Risposte:


171

Due cose:

  1. In generale, Java ha solo 2 livelli di ambito: globale e funzione. Ma provare / catturare è un'eccezione (nessun gioco di parole previsto). Quando viene generata un'eccezione e all'oggetto eccezione viene assegnata una variabile, tale variabile oggetto è disponibile solo all'interno della sezione "cattura" e viene distrutta non appena la cattura viene completata.

  2. (e ancora più importante). Non puoi sapere dove è stata generata l'eccezione nel blocco try. Potrebbe essere stato prima che la tua variabile fosse dichiarata. Pertanto è impossibile dire quali variabili saranno disponibili per la clausola catch / finally. Considera il caso seguente, in cui l'ambito è come hai suggerito:

    
    try
    {
        throw new ArgumentException("some operation that throws an exception");
        string s = "blah";
    }
    catch (e as ArgumentException)
    {  
        Console.Out.WriteLine(s);
    }

Questo chiaramente è un problema: quando si raggiunge il gestore delle eccezioni, i messaggi non verranno dichiarati. Dato che le catture hanno lo scopo di gestire circostanze eccezionali e che infine devono essere eseguite, essere sicuri e dichiararlo un problema in fase di compilazione è molto meglio che in fase di esecuzione.


55

Come hai potuto essere sicuro di aver raggiunto la parte della dichiarazione nel blocco di cattura? E se l'istanza genera l'eccezione?


6
Eh? Le dichiarazioni variabili non generano eccezioni.
Giosuè,

6
D'accordo, è l'istanza che potrebbe generare l'eccezione.
Burkhard,

19

Tradizionalmente, nei linguaggi in stile C, ciò che accade all'interno delle parentesi graffe rimane all'interno delle parentesi graffe. Penso che avere la durata di una variabile si estenda attraverso ambiti del genere non sarebbe intuitivo per la maggior parte dei programmatori. Puoi ottenere ciò che desideri racchiudendo i blocchi try / catch / finally all'interno di un altro livello di parentesi graffe. per esempio

... code ...
{
    string s = "test";
    try
    {
        // more code
    }
    catch(...)
    {
        Console.Out.WriteLine(s);
    }
}

EDIT: Credo che ogni regola non ha un'eccezione. Quanto segue è C ++ valido:

int f() { return 0; }

void main() 
{
    int y = 0;

    if (int x = f())
    {
        cout << x;
    }
    else
    {
        cout << x;
    }
}

L'ambito di x è il condizionale, la clausola then e la clausola else.


10

Tutti gli altri hanno sollevato le basi: ciò che accade in un blocco rimane in un blocco. Ma nel caso di .NET, può essere utile esaminare ciò che il compilatore pensa stia accadendo. Prendi, ad esempio, il seguente codice try / catch (nota che StreamReader è dichiarato, correttamente, al di fuori dei blocchi):

static void TryCatchFinally()
{
    StreamReader sr = null;
    try
    {
        sr = new StreamReader(path);
        Console.WriteLine(sr.ReadToEnd());
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.ToString());
    }
    finally
    {
        if (sr != null)
        {
            sr.Close();
        }
    }
}

Questo verrà compilato in qualcosa di simile al seguente in MSIL:

.method private hidebysig static void  TryCatchFinallyDispose() cil managed
{
  // Code size       53 (0x35)    
  .maxstack  2    
  .locals init ([0] class [mscorlib]System.IO.StreamReader sr,    
           [1] class [mscorlib]System.Exception ex)    
  IL_0000:  ldnull    
  IL_0001:  stloc.0    
  .try    
  {    
    .try    
    {    
      IL_0002:  ldsfld     string UsingTest.Class1::path    
      IL_0007:  newobj     instance void [mscorlib]System.IO.StreamReader::.ctor(string)    
      IL_000c:  stloc.0    
      IL_000d:  ldloc.0    
      IL_000e:  callvirt   instance string [mscorlib]System.IO.TextReader::ReadToEnd()
      IL_0013:  call       void [mscorlib]System.Console::WriteLine(string)    
      IL_0018:  leave.s    IL_0028
    }  // end .try
    catch [mscorlib]System.Exception 
    {
      IL_001a:  stloc.1
      IL_001b:  ldloc.1    
      IL_001c:  callvirt   instance string [mscorlib]System.Exception::ToString()    
      IL_0021:  call       void [mscorlib]System.Console::WriteLine(string)    
      IL_0026:  leave.s    IL_0028    
    }  // end handler    
    IL_0028:  leave.s    IL_0034    
  }  // end .try    
  finally    
  {    
    IL_002a:  ldloc.0    
    IL_002b:  brfalse.s  IL_0033    
    IL_002d:  ldloc.0    
    IL_002e:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()    
    IL_0033:  endfinally    
  }  // end handler    
  IL_0034:  ret    
} // end of method Class1::TryCatchFinallyDispose

Cosa vediamo? MSIL rispetta i blocchi - sono intrinsecamente parte del codice sottostante generato quando compili il tuo C #. L'ambito non è solo definito nelle specifiche C #, ma anche nelle specifiche CLR e CLS.

L'ambito ti protegge, ma a volte devi aggirarlo. Nel tempo, ti ci abitui e inizia a sentirsi naturale. Come hanno detto tutti gli altri, ciò che accade in un blocco rimane in quel blocco. Vuoi condividere qualcosa? Devi andare fuori dai blocchi ...


8

In C ++, comunque, l'ambito di una variabile automatica è limitato dalle parentesi graffe che la circondano. Perché qualcuno dovrebbe aspettarsi che questo sia diverso cancellando una parola chiave try al di fuori delle parentesi graffe?


1
Concordato; "}" significa fine ambito. Tuttavia, try-catch-finally è insolito in quanto dopo un blocco try, è necessario disporre di un blocco catch e / o infine; quindi, un'eccezione alla regola normale in cui l'ambito di un blocco try portato nella cattura associata / finalmente potrebbe sembrare accettabile?
Jon Schneider,

7

Come ha sottolineato Ravenspoint, tutti si aspettano che le variabili siano locali per il blocco in cui sono definite. tryIntroduce un blocco e così fa catch.

Se vuoi variabili locali in entrambi trye catch, prova a racchiudere entrambi in un blocco:

// here is some code
{
    string s;
    try
    {

        throw new Exception(":(")
    }
    catch (Exception e)
    {
        Debug.WriteLine(s);
    }
}

5

La semplice risposta è che C e la maggior parte dei linguaggi che hanno ereditato la sua sintassi sono a ambito di blocco. Ciò significa che se una variabile è definita in un blocco, ovvero all'interno di {}, questo è il suo ambito.

L'eccezione, a proposito, è JavaScript, che ha una sintassi simile, ma ha una funzione. In JavaScript, una variabile dichiarata in un blocco try è nell'ambito del blocco catch e ovunque nella sua funzione di contenimento.


4

@burkhard ha la domanda sul perché abbia risposto correttamente, ma come nota che volevo aggiungere, mentre l'esempio della soluzione raccomandata è buono 99.9999 +% di volte, non è una buona pratica, è molto più sicuro verificare la presenza di null prima di utilizzare qualcosa crea un'istanza all'interno del blocco try o inizializza la variabile su qualcosa invece di dichiararla prima del blocco try. Per esempio:

string s = String.Empty;
try
{
    //do work
}
catch
{
   //safely access s
   Console.WriteLine(s);
}

O:

string s;
try
{
    //do work
}
catch
{
   if (!String.IsNullOrEmpty(s))
   {
       //safely access s
       Console.WriteLine(s);
   }
}

Ciò dovrebbe fornire scalabilità nella soluzione alternativa, in modo che anche quando ciò che si sta facendo nel blocco try è più complesso dell'assegnazione di una stringa, si dovrebbe essere in grado di accedere in sicurezza ai dati dal blocco catch.


4

Secondo la sezione intitolata "Come gettare e catturare le eccezioni" nella lezione 2 del kit di formazione autonoma MCTS (esame 70-536): Microsoft® .NET Framework 2.0 - Application Development Foundation , il motivo è che potrebbe essersi verificata un'eccezione prima delle dichiarazioni delle variabili nel blocco try (come altri hanno già notato).

Citazione da pagina 25:

"Si noti che la dichiarazione StreamReader è stata spostata all'esterno del blocco Try nell'esempio precedente. Ciò è necessario perché il blocco Infine non può accedere alle variabili dichiarate all'interno del blocco Try. Ciò ha senso perché, a seconda di dove si è verificata un'eccezione, le dichiarazioni delle variabili all'interno del Il blocco Try potrebbe non essere stato ancora eseguito . "


4

La risposta, come tutti hanno sottolineato, è praticamente "ecco come vengono definiti i blocchi".

Ci sono alcune proposte per rendere il codice più bello. Vedi ARM

 try (FileReader in = makeReader(), FileWriter out = makeWriter()) {
       // code using in and out
 } catch(IOException e) {
       // ...
 }

Anche le chiusure dovrebbero affrontare questo.

with(FileReader in : makeReader()) with(FileWriter out : makeWriter()) {
    // code using in and out
}

AGGIORNAMENTO: ARM è implementato in Java 7. http://download.java.net/jdk7/docs/technotes/guides/language/try-with-resources.html


2

La tua soluzione è esattamente ciò che dovresti fare. Non puoi essere sicuro che la tua dichiarazione sia stata raggiunta nel blocco try, il che comporterebbe un'altra eccezione nel blocco catch.

Deve semplicemente funzionare come ambiti separati.

try
    dim i as integer = 10 / 0 ''// Throw an exception
    dim s as string = "hi"
catch (e)
    console.writeln(s) ''// Would throw another exception, if this was allowed to compile
end try

2

Le variabili sono a livello di blocco e limitate a quel blocco Try o Catch. Simile alla definizione di una variabile in un'istruzione if. Pensa a questa situazione.

try {    
    fileOpen("no real file Name");    
    String s = "GO TROJANS"; 
} catch (Exception) {   
    print(s); 
}

La stringa non verrebbe mai dichiarata, quindi non si può fare affidamento su di essa.


2

Perché il blocco try e il blocco catch sono 2 blocchi diversi.

Nel codice seguente, ti aspetteresti che s definiti nel blocco A siano visibili nel blocco B?

{ // block A
  string s = "dude";
}

{ // block B
  Console.Out.WriteLine(s); // or printf or whatever
}

2

Mentre nel tuo esempio è strano che non funzioni, prendi questo simile:

    try
    {
         //Code 1
         String s = "1|2";
         //Code 2
    }
    catch
    {
         Console.WriteLine(s.Split('|')[1]);
    }

Ciò indurrebbe il fermo a generare un'eccezione di riferimento null in caso di violazione del codice 1. Ora, mentre la semantica di try / catch è abbastanza ben compresa, questo sarebbe un fastidioso caso d'angolo, poiché s è definito con un valore iniziale, quindi in teoria non dovrebbe mai essere nullo, ma in semantica condivisa lo sarebbe.

Ancora una volta ciò potrebbe in teoria essere risolto consentendo solo definizioni separate ( String s; s = "1|2";), o qualche altra serie di condizioni, ma è generalmente più semplice dire semplicemente no.

Inoltre, consente di definire globalmente la semantica dell'ambito senza eccezioni, in particolare i locali durano fino a {}quando sono definiti, in tutti i casi. Punto minore, ma un punto.

Infine, per fare ciò che desideri, puoi aggiungere un set di parentesi attorno al catch di prova. Ti dà l'ambito che desideri, anche se ha un costo di leggibilità, ma non troppo.

{
     String s;
     try
     {
          s = "test";
          //More code
     }
     catch
     {
          Console.WriteLine(s);
     }
}

1

Nell'esempio specifico che hai fornito, l'inizializzazione di s non può generare un'eccezione. Quindi penseresti che forse il suo ambito potrebbe essere esteso.

Ma in generale, le espressioni inizializzatore possono generare eccezioni. Non avrebbe senso per una variabile il cui inizializzatore ha generato un'eccezione (o che è stata dichiarata dopo un'altra variabile in cui è accaduto) essere in grado di catturare / finalmente.

Inoltre, la leggibilità del codice ne risentirebbe. La regola in C (e nei linguaggi che la seguono, compresi C ++, Java e C #) è semplice: gli ambiti variabili seguono i blocchi.

Se si desidera che una variabile sia nell'ambito di try / catch / infine, ma in nessun altro luogo, quindi avvolgere il tutto in un altro set di parentesi graffe (un blocco nudo) e dichiarare la variabile prima del tentativo.


1

Parte del motivo per cui non rientrano nello stesso ambito è perché in qualsiasi punto del blocco try, è possibile aver generato l'eccezione. Se fossero nello stesso ambito, è un disastro in attesa, perché a seconda di dove è stata lanciata l'eccezione, potrebbe essere ancora più ambiguo.

Almeno quando viene dichiarato al di fuori del blocco try, sai con certezza quale potrebbe essere la variabile al minimo quando viene generata un'eccezione; Il valore della variabile prima del blocco try.


1

Quando si dichiara una variabile locale, questa viene posizionata nello stack (per alcuni tipi l'intero valore dell'oggetto sarà nello stack, per altri tipi solo uno riferimento sarà nello stack). Quando esiste un'eccezione all'interno di un blocco try, le variabili locali all'interno del blocco vengono liberate, il che significa che lo stack viene "srotolato" allo stato in cui si trovava all'inizio del blocco try. Questo è di progettazione. È così che il try / catch è in grado di uscire da tutte le chiamate di funzione all'interno del blocco e riporta il sistema in uno stato funzionale. Senza questo meccanismo non potresti mai essere sicuro dello stato di nulla quando si verifica un'eccezione.

Avere il tuo codice di gestione degli errori basato su variabili dichiarate esternamente che hanno i loro valori modificati all'interno del blocco try mi sembra una cattiva progettazione. Quello che stai facendo è essenzialmente perdere risorse intenzionalmente al fine di ottenere informazioni (in questo caso particolare non è così male perché stai solo perdendo informazioni, ma immagini se fosse qualche altra risorsa? Stai solo rendendo la vita più dura a te stesso nel futuro). Suggerirei di suddividere i blocchi di prova in blocchi più piccoli se si richiede maggiore granularità nella gestione degli errori.


1

Quando hai un tentativo di cattura, dovresti al massimo sapere che potrebbero essere generati errori. Queste classi di eccezioni normalmente dicono tutto ciò che serve sull'eccezione. In caso contrario, dovresti creare le tue classi di eccezioni e trasmettere tali informazioni. In questo modo, non avrai mai bisogno di ottenere le variabili dall'interno del blocco try, perché l'eccezione è autoesplicativa. Quindi, se hai bisogno di fare molto, pensa al tuo design e prova a pensare se esiste un altro modo, che puoi prevedere le eccezioni in arrivo o utilizzare le informazioni provenienti dalle eccezioni, e quindi forse ripensare le tue eccezione con maggiori informazioni.


1

Come è stato sottolineato da altri utenti, le parentesi graffe definiscono l'ambito in quasi tutti i linguaggi in stile C che conosco.

Se è una variabile semplice, allora perché ti importa quanto tempo rimarrà nell'ambito? Non è un grosso problema.

in C #, se è una variabile complessa, vorrai implementare IDisposable. È quindi possibile utilizzare try / catch / finally e chiamare obj.Dispose () nel blocco finally. Oppure puoi usare la parola chiave using, che chiamerà automaticamente Dispose alla fine della sezione del codice.


1

In Python sono visibili nei blocchi catch / finally se la linea che li dichiara non viene lanciata.


1

Che cosa succede se l'eccezione viene generata in un codice che si trova sopra la dichiarazione della variabile. Ciò significa che in questo caso la dichiarazione stessa non è avvenuta.

try {

       //doSomeWork // Exception is thrown in this line. 
       String s;
       //doRestOfTheWork

} catch (Exception) {
        //Use s;//Problem here
} finally {
        //Use s;//Problem here
}

1

La specifica C # (15.2) afferma "L'ambito di una variabile locale o costante dichiarata in un blocco è il blocco."

(nel tuo primo esempio il blocco try è il blocco in cui è dichiarata "s")


0

Il mio pensiero sarebbe che, poiché qualcosa nel blocco try ha innescato l'eccezione, il suo contenuto nello spazio dei nomi non può essere considerato attendibile, vale a dire che fare riferimento a String 's' nel blocco catch potrebbe causare l'ennesima eccezione.


0

Bene, se non genera un errore di compilazione e potresti dichiararlo per il resto del metodo, non ci sarebbe modo di dichiararlo solo nell'ambito di try. Ti costringe a essere esplicito su dove dovrebbe esistere la variabile e non fa ipotesi.


0

Se per un momento ignoriamo il problema dei blocchi di scoping, il compilatore dovrebbe lavorare molto di più in una situazione non ben definita. Sebbene ciò non sia impossibile, l'errore di scoping ti costringe anche, l'autore del codice, a comprendere le implicazioni del codice che scrivi (che la stringa s può essere nulla nel blocco catch). Se il tuo codice era legale, nel caso di un'eccezione OutOfMemory, non è nemmeno garantito che a s venga assegnato uno slot di memoria:

// won't compile!
try
{
    VeryLargeArray v = new VeryLargeArray(TOO_BIG_CONSTANT); // throws OutOfMemoryException
    string s = "Help";
}
catch
{
    Console.WriteLine(s); // whoops!
}

Il CLR (e quindi il compilatore) ti costringe anche a inizializzare le variabili prima che vengano utilizzate. Nel blocco di cattura presentato non può garantirlo.

Quindi finiamo con il compilatore che deve fare molto lavoro, che in pratica non offre molti benefici e probabilmente confonderebbe le persone e li indurrebbe a chiedere perché try / catch funziona in modo diverso.

Oltre alla coerenza, non consentendo a nulla di stravagante e aderente alla semantica di scoping già consolidata utilizzata in tutto il linguaggio, il compilatore e CLR sono in grado di fornire una maggiore garanzia dello stato di una variabile all'interno di un blocco catch. Che esiste ed è stato inizializzato.

Si noti che i progettisti del linguaggio hanno fatto un buon lavoro con altri costrutti come l' utilizzo e il blocco in cui il problema e l'ambito sono ben definiti, il che consente di scrivere codice più chiaro.

ad es. la parola chiave using con oggetti IDisposable in:

using(Writer writer = new Writer())
{
    writer.Write("Hello");
}

è equivalente a:

Writer writer = new Writer();
try
{        
    writer.Write("Hello");
}
finally
{
    if( writer != null)
    {
        ((IDisposable)writer).Dispose();
    }
}

Se il tuo try / catch / finalmente è difficile da capire, prova a refactoring o ad introdurre un altro livello di indiretta con una classe intermedia che incapsula la semantica di ciò che stai cercando di realizzare. Senza vedere il vero codice, è difficile essere più specifici.


0

Invece di una variabile locale, è possibile dichiarare una proprietà pubblica; anche questo dovrebbe evitare un altro potenziale errore di una variabile non assegnata. stringa pubblica S {get; impostato; }


-1

Se l'operazione di assegnazione fallisce, l'istruzione catch avrà un riferimento null alla variabile non assegnata.


2
Non è assegnato. Non è nemmeno nullo (a differenza dell'istanza e delle variabili statiche).
Tom Hawtin - tackline

-1

C # 3.0:

string html = new Func<string>(() =>
{
    string webpage;

    try
    {
        using(WebClient downloader = new WebClient())
        {
            webpage = downloader.DownloadString(url);
        }
    }
    catch(WebException)
    {
        Console.WriteLine("Download failed.");  
    }

    return webpage;
})();

WTF? Perché il voto negativo? L'incapsulamento è parte integrante di OOP. Sembra anche carino.
core

2
Non ero il downvote, ma ciò che è sbagliato è restituire una stringa non inizializzata.
Ben Voigt,
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.