In C #, perché le variabili dichiarate all'interno di un blocco try sono limitate nell'ambito?


23

Voglio aggiungere la gestione degli errori a:

var firstVariable = 1;
var secondVariable = firstVariable;

Il seguito non verrà compilato:

try
{
   var firstVariable = 1;
}
catch {}

try
{
   var secondVariable = firstVariable;
}
catch {}

Perché è necessario che un blocco try try influisca sull'ambito delle variabili come fanno altri blocchi di codice? Coerenza a parte, non avrebbe senso per noi essere in grado di avvolgere il nostro codice con la gestione degli errori senza la necessità di refactoring?


14
A try.. catchè un tipo specifico di blocco di codice e, per quanto riguarda tutti i blocchi di codice, non è possibile dichiarare una variabile in una e utilizzare quella stessa variabile in un'altra come questione di ambito.
Neil

"è un tipo specifico di blocco di codice". Specifico in che modo? Grazie
JᴀʏMᴇᴇ

7
Voglio dire, qualsiasi cosa tra parentesi graffe è un blocco di codice. Lo vedi seguendo un'istruzione if e seguendo un'istruzione for, sebbene il concetto sia lo stesso. Il contenuto è in un ambito elevato rispetto al suo ambito padre. Sono abbastanza sicuro che questo sarebbe problematico se avessi usato le parentesi graffe {}senza provare.
Neil,

Suggerimento: tieni presente che l'utilizzo di (IDisposable) {} e solo {} si applica anche allo stesso modo. Quando si utilizza un utilizzo con un IDisposable, pulirà automaticamente le risorse indipendentemente dall'esito positivo o negativo. Ci sono alcune eccezioni a questo, come non tutte le classi che ti aspetteresti di implementare IDisposable ...
Julia McGuigan

1
Molte discussioni su questa stessa domanda su StackOverflow, qui: stackoverflow.com/questions/94977/…
Jon Schneider

Risposte:


90

E se il tuo codice fosse:

try
{
   MethodThatMightThrow();
   var firstVariable = 1;
}
catch {}

try
{
   var secondVariable = firstVariable;
}
catch {}

Ora proveresti a usare una variabile non dichiarata ( firstVariable) se la tua chiamata al metodo genera.

Nota : l'esempio sopra risponde in modo specifico alla domanda originale, che afferma "coerenza-a parte". Ciò dimostra che ci sono ragioni diverse dalla coerenza. Ma, come dimostra la risposta di Peter, esiste anche un forte argomento di coerenza, che sicuramente sarebbe stato un fattore molto importante nella decisione.


Ah, questo è esattamente quello che stavo cercando. Sapevo che c'erano alcune caratteristiche linguistiche che rendevano impossibile ciò che stavo suggerendo, ma non riuscivo a trovare alcun scenario. Grazie mille.
JᴀʏMᴇᴇ

1
"Ora proveresti a utilizzare una variabile non dichiarata se la tua chiamata al metodo genera." Inoltre, supponiamo che ciò sia stato evitato trattando la variabile come se fosse stata dichiarata, ma non inizializzata, prima del codice che potrebbe essere lanciato. Quindi non sarebbe non dichiarato, ma sarebbe comunque potenzialmente non assegnato e un'analisi di assegnazione definita proibirebbe la lettura del suo valore (senza un incarico intermedio che potrebbe dimostrarsi accaduto).
Eliah Kagan,

3
Per un linguaggio statico come C #, la dichiarazione è realmente rilevante solo al momento della compilazione. Il compilatore potrebbe facilmente spostare la dichiarazione in precedenza nell'ambito. Il fatto più importante in fase di esecuzione è che la variabile potrebbe non essere inizializzata .
jpmc26,

3
Non sono d'accordo con questa risposta. C # ha già la regola che le variabili non inizializzate non possono essere lette, con una certa consapevolezza del flusso di dati. (Prova a dichiarare le variabili in caso di a switche ad accedervi in ​​altri.) Questa regola potrebbe facilmente applicarsi qui e impedire comunque la compilazione di questo codice. Penso che la risposta di Peter qui sotto sia più plausibile.
Sebastian Redl,

2
C'è una differenza tra non dichiarato e non inizializzato e C # li segue separatamente. Se ti fosse permesso di usare una variabile al di fuori del blocco in cui è stata dichiarata, ciò significherebbe che potresti assegnarla ad essa nel primo catchblocco e quindi sarebbe sicuramente assegnata nel secondo tryblocco.
svick,

64

So che Ben ha risposto bene, ma volevo affrontare la coerenza POV che è stata convenientemente messa da parte. Supponendo che i try/catchblocchi non abbiano influenzato l'ambito, quindi si otterrebbe:

{
    // new scope here
}

try
{
   // Not new scope
}

E per me questo si schianta a testa in Principio del minimo stupore (POLA) perché ora hai il doppio dovere {e lo }fai a seconda del contesto di ciò che li ha preceduti.

L'unica via d'uscita da questo pasticcio è designare qualche altro marker per delineare i try/catchblocchi. Che inizia ad aggiungere un odore di codice. Quindi, dal momento che non hai ambito try/catchnella lingua, sarebbe stato un tale casino che sarebbe stato meglio con la versione con ambito.


Un'altra risposta eccellente. E non avevo mai sentito parlare di POLA, così bella lettura ulteriore. Grazie mille amico.
JᴀʏMᴇᴇ

"L'unica via d'uscita da questo pasticcio è designare qualche altro marker per delineare try/ catchblocchi." - vuoi dire try { { // scope } }:? :)
CompuChip

@CompuChip che avrebbe {}tirato il doppio dovere come ambito e non creando ambito a seconda del contesto. try^ //no-scope ^sarebbe un esempio di un marcatore diverso.
Leliel,

1
Secondo me, questa è la ragione molto più fondamentale e più vicina alla risposta "reale".
Jack Aidley,

@JackAidley soprattutto perché puoi già scrivere codice in cui usi una variabile che potrebbe non essere assegnata. Quindi, sebbene la risposta di Ben abbia un punto su come questo sia un comportamento utile, non lo vedo come il motivo per cui esiste il comportamento. La risposta di Ben rileva l'OP che dice "per amor di coerenza a parte", ma la coerenza è una ragione perfettamente valida! L'ambito ristretto offre numerosi altri vantaggi.
Kat,

21

Coerenza a parte, non avrebbe senso per noi essere in grado di avvolgere il nostro codice con la gestione degli errori senza la necessità di refactoring?

Per rispondere a questo, è necessario guardare oltre l'ambito di una variabile .

Anche se la variabile rimanesse nell'ambito, non sarebbe sicuramente assegnata .

Dichiarare la variabile nel blocco try esprime - per il compilatore e per i lettori umani - che è significativa solo all'interno di quel blocco. È utile che il compilatore lo imponga.

Se si desidera che la variabile sia nell'ambito nell'ambito del blocco try, è possibile dichiararla al di fuori del blocco:

var zerothVariable = 1_000_000_000_000L;
int firstVariable;

try {
    // Change checked to unchecked to allow the overflow without throwing.
    firstVariable = checked((int)zerothVariable);
}
catch (OverflowException e) {
    Console.Error.WriteLine(e.Message);
    Environment.Exit(1);
}

Ciò indica che la variabile può essere significativa al di fuori del blocco try. Il compilatore lo consentirà.

Ma mostra anche un altro motivo per cui di solito non sarebbe utile mantenere le variabili nell'ambito dopo averle introdotte in un blocco try. Il compilatore C # esegue analisi di assegnazione definite e vieta di leggere il valore di una variabile che non ha dimostrato di aver ricevuto un valore.Quindi non puoi ancora leggere dalla variabile.

Supponiamo che provo a leggere dalla variabile dopo il blocco try:

Console.WriteLine(firstVariable);

Ciò darà un errore in fase di compilazione :

CS0165 Utilizzo della variabile locale non assegnata 'firstVariable'

Ho chiamato Environment.Exit nel blocco catch, così io conosco la variabile è stato assegnato prima della chiamata a Console.WriteLine. Ma il compilatore non ne deduce.

Perché il compilatore è così rigoroso?

Non riesco nemmeno a fare questo:

int n;

try {
    n = 10; // I know this won't throw an IOException.
}
catch (IOException) {
}

Console.WriteLine(n);

Un modo per esaminare questa limitazione è dire che l'analisi di assegnazione definita in C # non è molto sofisticata. Ma un altro modo di vederlo è che, quando scrivi il codice in un blocco try con clausole catch, stai dicendo sia al compilatore che a tutti i lettori umani che dovrebbe essere trattato come se non fosse possibile eseguirlo.

Per illustrare cosa intendo, immagina se il compilatore consentisse il codice sopra, ma poi hai aggiunto una chiamata nel blocco try a una funzione che conosci personalmente non genererà un'eccezione . Non essendo in grado di garantire che la funzione chiamata non abbia lanciato un IOException, il compilatore non poteva sapere che nera stato assegnato e quindi si sarebbe dovuto eseguire il refactoring.

Questo per dire che, rinunciando ad analisi altamente sofisticate nel determinare se una variabile assegnata in un blocco try con clausole catch è stata definitivamente assegnata in seguito, il compilatore ti aiuta a evitare di scrivere codice che probabilmente si interromperà in seguito. (Dopo tutto, catturare un'eccezione di solito significa che pensi che uno potrebbe essere lanciato.)

Puoi assicurarti che la variabile sia assegnata attraverso tutti i percorsi del codice.

È possibile compilare il codice assegnando alla variabile un valore prima del blocco try o nel blocco catch. In questo modo, sarà comunque stato inizializzato o assegnato, anche se l'assegnazione nel blocco try non ha luogo. Per esempio:

var n = 0; // But is this meaningful, or just covering a bug?

try {
    n = 10;
}
catch (IOException) {
}

Console.WriteLine(n);

O:

int n;

try {
    n = 10;
}
catch (IOException) {
    n = 0; // But is this meaningful, or just covering a bug?
}

Console.WriteLine(n);

Questi compilano. Ma è meglio fare qualcosa del genere solo se il valore predefinito che hai dato ha senso * e produce un comportamento corretto.

Si noti che, in questo secondo caso in cui si assegna la variabile nel blocco try e in tutti i blocchi catch, sebbene sia possibile leggere la variabile dopo il try-catch, non sarà ancora possibile leggere la variabile all'interno di un finallyblocco collegato , perché l'esecuzione può lasciare un blocco try in più situazioni di quanto pensiamo spesso .

* A proposito, alcuni linguaggi, come C e C ++, entrambi consentono variabili non inizializzate e non hanno un'analisi di assegnazione definita per impedire la lettura da essi. Poiché la lettura di una memoria non inizializzata fa sì che i programmi si comportino in modo non deterministico ed erratico , si consiglia generalmente di evitare di introdurre variabili in tali lingue senza fornire un inizializzatore. In linguaggi con analisi di assegnazione definita come C # e Java, il compilatore ti salva dalla lettura di variabili non inizializzate e anche dal male minore di inizializzarle con valori insignificanti che possono in seguito essere interpretati erroneamente come significativi.

Puoi farlo in modo che i percorsi del codice in cui la variabile non sia assegnata generino un'eccezione (o ritornino).

Se si prevede di eseguire alcune azioni (come la registrazione) e riproporre l'eccezione o generare un'altra eccezione, e ciò accade in tutte le clausole catch in cui la variabile non è assegnata, il compilatore saprà che la variabile è stata assegnata:

int n;

try {
    n = 10;
}
catch (IOException e) {
    Console.Error.WriteLine(e.Message);
    throw;
}

Console.WriteLine(n);

Questo compila e potrebbe essere una scelta ragionevole. Tuttavia, in un'applicazione reale, a meno che l'eccezione non venga generata solo in situazioni in cui non ha nemmeno senso tentare di recuperare * , è necessario assicurarsi che si stia ancora rilevando e gestendo correttamente da qualche parte .

(Non è possibile leggere la variabile in un blocco finally in questa situazione, ma non sembra che dovresti essere in grado di farlo - dopotutto, i blocchi finalmente essenzialmente vengono sempre eseguiti, e in questo caso la variabile non viene sempre assegnata .)

* Ad esempio, molte applicazioni non hanno una clausola catch che gestisca una OutOfMemoryException perché tutto ciò che potrebbero fare al riguardo potrebbe essere almeno grave quanto il crash .

Forse davvero non vuole refactoring del codice.

Nel tuo esempio, introduci firstVariablee secondVariablenei blocchi di prova. Come ho già detto, puoi definirli prima dei blocchi di prova in cui sono assegnati in modo che rimangano nell'ambito dopo, e puoi soddisfare / ingannare il compilatore per permetterti di leggerli assicurandoti che siano sempre assegnati.

Ma il codice che appare dopo quei blocchi presumibilmente dipende dal fatto che sono stati assegnati correttamente. In tal caso, il codice dovrebbe riflettere e assicurarsi che.

In primo luogo, puoi (e dovresti) effettivamente gestire l'errore lì? Uno dei motivi per cui esiste la gestione delle eccezioni è quello di semplificare la gestione degli errori in cui possono essere gestiti in modo efficace , anche se non si trovano nelle vicinanze.

Se non riesci effettivamente a gestire l'errore nella funzione che ha inizializzato e utilizza quelle variabili, forse il blocco try non dovrebbe trovarsi in quella funzione, ma piuttosto in un punto più alto (ad esempio, nel codice che chiama quella funzione o codice che chiama quel codice). Assicurati solo di non rilevare accidentalmente un'eccezione generata altrove e di pensare erroneamente che sia stata generata durante l'inizializzazione firstVariablee secondVariable.

Un altro approccio è quello di inserire il codice che utilizza le variabili nel blocco try. Questo è spesso ragionevole. Ancora una volta, se le stesse eccezioni che stai rilevando dai loro inizializzatori potrebbero anche essere generate dal codice circostante, dovresti assicurarti di non trascurare quella possibilità quando li gestisci.

(Suppongo che tu stia inizializzando le variabili con espressioni più complicate di quelle mostrate nei tuoi esempi, in modo che possano effettivamente generare un'eccezione e anche che non stai davvero pianificando di catturare tutte le possibili eccezioni , ma solo di catturare qualunque eccezione specifica puoi anticipare e gestire in modo significativo . È vero che il mondo reale non è sempre così bello e il codice di produzione a volte lo fa , ma poiché il tuo obiettivo qui è gestire gli errori che si verificano durante l'inizializzazione di due variabili specifiche, qualsiasi clausola di cattura che scrivi per quella specifica lo scopo dovrebbe essere specifico per qualsiasi errore si tratti.)

Un terzo modo è quello di estrarre il codice che può fallire e il try-catch che lo gestisce nel proprio metodo. Ciò è utile se si desidera prima affrontare gli errori completamente e quindi non preoccuparsi di rilevare inavvertitamente un'eccezione che dovrebbe essere gestita da qualche altra parte.

Supponiamo, ad esempio, di voler chiudere immediatamente l'applicazione in caso di mancata assegnazione di entrambe le variabili. (Ovviamente non tutta la gestione delle eccezioni è per errori fatali; questo è solo un esempio e potrebbe essere o meno il modo in cui l'applicazione deve reagire al problema.) Potresti fare qualcosa del genere:

// In real life, this should be named more descriptively.
private static (int firstValue, int secondValue) GetFirstAndSecondValues()
{
    try {
        // This code is contrived. The idea here is that obtaining the values
        // could actually fail, and throw a SomeSpecificException.
        var firstVariable = 1;
        var secondVariable = firstVariable;
        return (firstVariable, secondVariable);
    }
    catch (SomeSpecificException e) {
        Console.Error.WriteLine(e.Message);
        Environment.Exit(1);
        throw new InvalidOperationException(); // unreachable
    }
}

// ...and of course so should this.
internal static void MethodThatUsesTheValues()
{
    var (firstVariable, secondVariable) = GetFirstAndSecondValues();

    // Code that does something with them...
}

Quel codice restituisce e decostruisce una ValueTuple con la sintassi di C # 7.0 per restituire più valori, ma se si utilizza ancora una versione precedente di C #, è comunque possibile utilizzare questa tecnica; ad esempio, è possibile utilizzare i parametri o restituire un oggetto personalizzato che fornisce entrambi i valori . Inoltre, se le due variabili non sono in realtà strettamente correlate, probabilmente sarebbe meglio avere comunque due metodi separati.

Soprattutto se si dispone di più metodi del genere, è consigliabile considerare la centralizzazione del codice per avvisare l'utente di errori fatali e la chiusura. (Ad esempio, è possibile scrivere un Diemetodo con un messageparametro.) La throw new InvalidOperationException();riga non viene mai effettivamente eseguita, quindi non è necessario (e non è necessario) scrivere una clausola catch per esso.

Oltre a chiudere quando si verifica un errore particolare, a volte potresti scrivere codice simile a questo se si genera un'eccezione di un altro tipo che racchiude l'eccezione originale . (In tale situazione, si avrebbe non bisogno di un secondo, un'espressione tiro irraggiungibile.)

Conclusione: l'ambito è solo una parte dell'immagine.

Puoi ottenere l'effetto di racchiudere il tuo codice con la gestione degli errori senza refactoring (o, se preferisci, con quasi nessun refactoring), semplicemente separando le dichiarazioni delle variabili dalle loro assegnazioni. Il compilatore lo consente se si soddisfano le regole di assegnazione definite di C # e la dichiarazione di una variabile prima del blocco try ne chiarisce l'ambito più ampio. Ma il refactoring può essere comunque l'opzione migliore.


"quando scrivi codice in un blocco try con clausole catch, stai dicendo sia al compilatore che a tutti i lettori umani che dovrebbe essere trattato come se non tutti fossero in grado di funzionare". Ciò che interessa al compilatore è che il controllo può raggiungere istruzioni successive anche se le dichiarazioni precedenti generano un'eccezione. Il compilatore presuppone normalmente che se un'istruzione genera un'eccezione, l'istruzione successiva non verrà eseguita, quindi una variabile non assegnata non verrà letta. L'aggiunta di un 'catch' consentirà al controllo di raggiungere le istruzioni successive: è la cattura che conta, non se il codice nel blocco try viene lanciato.
Pete Kirkham,
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.