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 n
era 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 finally
blocco 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 firstVariable
e secondVariable
nei 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 firstVariable
e 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 Die
metodo con un message
parametro.) 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.
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.