Quiero agregar manejo de errores a:
var firstVariable = 1;
var secondVariable = firstVariable;
Lo siguiente no se compilará:
try
{
var firstVariable = 1;
}
catch {}
try
{
var secondVariable = firstVariable;
}
catch {}
¿Por qué es necesario que un bloque try catch afecte el alcance de las variables como lo hacen otros bloques de código? 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?
c#
.net
error-handling
language-features
JᴀʏMᴇᴇ
fuente
fuente
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.{}
sin intentarlo.Respuestas:
¿Qué pasa si su código era:
Ahora intentaría usar una variable no declarada (
firstVariable
) si su llamada al método arroja.Nota : El ejemplo anterior responde específicamente a la pregunta original, que dice "dejando de lado la coherencia". Esto demuestra que hay otras razones además de la consistencia. Pero como muestra la respuesta de Peter, también hay un poderoso argumento de coherencia, que seguramente habría sido un factor muy importante en la decisión.
fuente
switch
y acceder a ellas en otros). Esta regla podría aplicarse fácilmente aquí y evitar que este código se compile de todos modos. Creo que la respuesta de Peter a continuación es más plausible.catch
bloque y luego definitivamente se asignaría en el segundotry
bloque.Sé que Ben ha respondido bien a esto, pero quería abordar el punto de vista de coherencia que convenientemente se hizo a un lado. Suponiendo que los
try/catch
bloques no afectaron el alcance, terminaría con:Y para mí, esto choca de frente con el Principio de menor asombro (POLA) porque ahora tienes el doble deber
{
y el}
deber dependiendo del contexto de lo que los precedió.La única forma de salir de este desastre es designar algún otro marcador para delinear
try/catch
bloques. Que comienza a agregar un código de olor. Entonces, para cuando no tenga alcancetry/catch
en el idioma, habría sido un desastre que hubiera sido mejor con la versión de alcance.fuente
try
/catch
bloquear". - quieres decirtry { { // scope } }
:? :){}
doble deber como alcance y no creación de alcance dependiendo del contexto todavía.try^ //no-scope ^
sería un ejemplo de un marcador diferente.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:
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:
Eso dará un error en tiempo de compilación :
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:
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 quen
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:
O:
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:
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
ysecondVariable
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
ysecondVariable
.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:
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 unmessage
parámetro). Lathrow 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.
fuente