Dejando a un lado la coherencia, ¿no tendría sentido para nosotros poder envolver nuestro código con manejo de errores sin la necesidad de refactorizar?
Para responder a esto, es necesario mirar más allá del alcance de una variable .
Incluso si la variable permaneciera dentro del alcance, no se asignaría definitivamente .
La declaración de la variable en el bloque try expresa, para el compilador y para los lectores humanos, que solo tiene sentido dentro de ese bloque. Es útil para el compilador hacer cumplir eso.
Si desea que la variable esté dentro del alcance después del bloque try, puede declararla fuera del bloque:
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);
}
Eso expresa que la variable puede ser significativa fuera del bloque try. El compilador lo permitirá.
Pero también muestra otra razón por la que generalmente no sería útil mantener variables en el alcance después de introducirlas en un bloque try. El compilador de C # realiza un análisis de asignación definitivo y prohíbe leer el valor de una variable que no ha demostrado que se le haya dado un valor. Entonces todavía no puede leer de la variable.
Supongamos que intento leer de la variable después del bloque try:
Console.WriteLine(firstVariable);
Eso dará un error en tiempo de compilación :
CS0165 Uso de la variable local no asignada 'firstVariable'
Llamé a Environment.Exit en el bloque catch, por lo que sé que la variable se ha asignado antes de la llamada a Console.WriteLine. Pero el compilador no infiere esto.
¿Por qué es tan estricto el compilador?
Ni siquiera puedo hacer esto:
int n;
try {
n = 10; // I know this won't throw an IOException.
}
catch (IOException) {
}
Console.WriteLine(n);
Una forma de ver esta restricción es decir que el análisis de asignación definitiva en C # no es muy sofisticado. Pero otra forma de verlo es que, cuando escribe código en un bloque de prueba con cláusulas catch, le está diciendo tanto al compilador como a los lectores humanos que debe tratarse como si no todos pudieran ejecutarse.
Para ilustrar lo que quiero decir, imagine si el compilador permitió el código anterior, pero luego agregó una llamada en el bloque try a una función que personalmente sabe que no arrojará una excepción . Al no poder garantizar que la función llamada no arrojó un IOException
, el compilador no podía saber que n
estaba asignado, y luego tendría que refactorizar.
Esto quiere decir que, al renunciar a un análisis altamente sofisticado para determinar si una variable asignada en un bloque de prueba con cláusulas catch se ha asignado definitivamente después, el compilador lo ayuda a evitar escribir código que probablemente se rompa más tarde. (Después de todo, detectar una excepción generalmente significa que crees que se podría lanzar una).
Puede asegurarse de que la variable se asigne a través de todas las rutas de código.
Puede compilar el código dando a la variable un valor antes del bloque try o en el bloque catch. De esa manera, aún se habrá inicializado o asignado, incluso si la asignación en el bloque try no tiene lugar. Por ejemplo:
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);
Aquellos compilan. Pero es mejor hacer algo así si el valor predeterminado que le da tiene sentido * y produce un comportamiento correcto.
Tenga en cuenta que, en este segundo caso, donde asigna la variable en el bloque try y en todos los bloques catch, aunque puede leer la variable después del try-catch, aún no podrá leer la variable dentro de un finally
bloque adjunto , porque la ejecución puede dejar un bloque de prueba en más situaciones de las que a menudo pensamos .
* Por cierto, algunos lenguajes, como C y C ++, permiten variables no inicializadas y no tienen un análisis de asignación definitivo para evitar su lectura. Debido a que leer memoria no inicializada hace que los programas se comporten de manera no determinista y errática , generalmente se recomienda evitar introducir variables en esos idiomas sin proporcionar un inicializador. En lenguajes con análisis de asignación definidos como C # y Java, el compilador le evita leer variables no inicializadas y también el mal menor de inicializarlas con valores sin sentido que luego pueden malinterpretarse como significativos.
Puede hacerlo para que las rutas de código donde la variable no está asignada arroje una excepción (o retorno).
Si planea realizar alguna acción (como iniciar sesión) y volver a lanzar la excepción o lanzar otra excepción, y esto sucede en cualquier cláusula catch donde la variable no está asignada, entonces el compilador sabrá que la variable ha sido asignada:
int n;
try {
n = 10;
}
catch (IOException e) {
Console.Error.WriteLine(e.Message);
throw;
}
Console.WriteLine(n);
Eso compila, y bien puede ser una opción razonable. Sin embargo, en una aplicación real, a menos que la excepción solo se produzca en situaciones en las que ni siquiera tiene sentido tratar de recuperarse * , debe asegurarse de que todavía lo está atrapando y manejando adecuadamente en algún lugar .
(Tampoco puede leer la variable en un bloque finalmente en esta situación, pero no parece que deba ser capaz, después de todo, los bloques finalmente siempre se ejecutan, y en este caso la variable no siempre se asigna .)
* Por ejemplo, muchas aplicaciones no tienen una cláusula catch que maneje una excepción OutOfMemoryException porque cualquier cosa que puedan hacer al respecto podría ser al menos tan mala como bloquearse .
Tal vez usted realmente no desea refactorizar el código.
En su ejemplo, introduce firstVariable
y secondVariable
prueba bloques. Como he dicho, puede definirlos antes de los bloques de prueba en los que están asignados para que luego permanezcan dentro del alcance, y puede satisfacer / engañar al compilador para que le permita leer de ellos asegurándose de que siempre estén asignados.
Pero el código que aparece después de esos bloques probablemente depende de que se hayan asignado correctamente. Si ese es el caso, entonces su código debe reflejar y garantizar eso.
Primero, ¿puede (y debería) manejar el error allí? Una de las razones por las que existe el manejo de excepciones es para facilitar el manejo de errores donde pueden manejarse de manera efectiva , incluso si eso no está cerca de donde ocurren.
Si no puede manejar el error en la función que se inicializó y usa esas variables, entonces quizás el bloque try no debería estar en esa función, sino en algún lugar más alto (es decir, en un código que llama a esa función o código eso llama a ese código). Solo asegúrate de no estar atrapando accidentalmente una excepción lanzada en otro lugar y asumiendo erróneamente que fue lanzada mientras se inicializa firstVariable
y secondVariable
.
Otro enfoque es colocar el código que usa las variables en el bloque try. Esto a menudo es razonable. Una vez más, si las mismas excepciones que está captando de sus inicializadores también se pueden generar desde el código circundante, debe asegurarse de no descuidar esa posibilidad cuando las maneje.
(Supongo que está inicializando las variables con expresiones más complicadas que las que se muestran en sus ejemplos, de modo que en realidad podrían arrojar una excepción, y también que realmente no está planeando capturar todas las excepciones posibles , sino solo detectar las excepciones específicas puede anticipar y manejar de manera significativa . Es cierto que el mundo real no siempre es tan agradable y el código de producción a veces hace esto , pero dado que su objetivo aquí es manejar los errores que ocurren mientras se inicializan dos variables específicas, cualquier cláusula de captura que escriba para ese específico El propósito debe ser específico para los errores que sean).
Una tercera forma es extraer el código que puede fallar, y el try-catch que lo maneja, en su propio método. Esto es útil si primero desea lidiar con los errores por completo, y luego no preocuparse por detectar inadvertidamente una excepción que debería ser manejada en otro lugar.
Supongamos, por ejemplo, que desea salir inmediatamente de la aplicación si no se asigna ninguna de las variables. (Obviamente, no todo el manejo de excepciones es para errores fatales; este es solo un ejemplo, y puede o no ser cómo desea que su aplicación reaccione al problema). Podría hacer algo como esto:
// 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...
}
Ese código regresa y deconstruye un ValueTuple con la sintaxis de C # 7.0 para devolver múltiples valores, pero si todavía está en una versión anterior de C #, aún puede usar esta técnica; por ejemplo, puede usar parámetros o devolver un objeto personalizado que proporcione ambos valores . Además, si las dos variables no están realmente estrechamente relacionadas, probablemente sería mejor tener dos métodos separados de todos modos.
Especialmente si tiene múltiples métodos como ese, debería considerar centralizar su código para notificar al usuario de errores fatales y dejar de fumar. (Por ejemplo, podría escribir un Die
método con un message
parámetro). La throw new InvalidOperationException();
línea nunca se ejecuta realmente, por lo que no necesita (y no debe) escribir una cláusula catch para ella.
Además de salir cuando se produce un error en particular, a veces puede escribir código que se vea así si lanza una excepción de otro tipo que envuelve la excepción original . (En esa situación, no necesitaría una segunda expresión de lanzamiento inalcanzable).
Conclusión: el alcance es solo una parte de la imagen.
Puede lograr el efecto de envolver su código con manejo de errores sin refactorizar (o, si lo prefiere, sin casi ninguna refactorización), simplemente separando las declaraciones de las variables de sus asignaciones. El compilador permite esto si cumple con las reglas de asignación definidas de C #, y si declara una variable antes del bloque try deja en claro su alcance mayor. Pero refactorizar aún más puede ser su mejor opción.
try.. catch
es un tipo específico de bloque de código, y en lo que respecta a todos los bloques de código, no puede declarar una variable en uno y usar esa misma variable en otro como una cuestión de alcance.