¿Por qué las variables no se declaran en "intentar" en el alcance de "captura" o "finalmente"?

139

En C # y en Java (y posiblemente también en otros lenguajes), las variables declaradas en un bloque "try" no están dentro del alcance en los bloques "catch" o "finalmente" correspondientes. Por ejemplo, el siguiente código no se compila:

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

En este código, se produce un error en tiempo de compilación en la referencia a s en el bloque catch, porque s solo está dentro del alcance en el bloque try. (En Java, el error de compilación es "s no se puede resolver"; en C #, es "El nombre 's' no existe en el contexto actual").

La solución general a este problema parece ser declarar variables justo antes del bloque try, en lugar de dentro del bloque try:

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

Sin embargo, al menos para mí, (1) esto se siente como una solución torpe, y (2) da como resultado que las variables tengan un alcance mayor que el programador previsto (todo el resto del método, en lugar de solo en el contexto del intentar-atrapar-finalmente).

Mi pregunta es, ¿cuáles fueron / son las razones detrás de esta decisión de diseño de lenguaje (en Java, en C # y / o en cualquier otro lenguaje aplicable)?

Jon Schneider
fuente

Respuestas:

171

Dos cosas:

  1. En general, Java tiene solo 2 niveles de alcance: global y funcional. Pero, try / catch es una excepción (sin juego de palabras). Cuando se lanza una excepción y el objeto de excepción obtiene una variable asignada, esa variable de objeto solo está disponible dentro de la sección "captura" y se destruye tan pronto como se completa la captura.

  2. (y más importante). No puede saber en qué parte del bloque try se produjo la excepción. Puede haber sido antes de que se declarara su variable. Por lo tanto, es imposible decir qué variables estarán disponibles para la cláusula catch / finally. Considere el siguiente caso, donde el alcance es como lo sugirió:

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

Esto claramente es un problema: cuando llegue al controlador de excepciones, no se declararán los correos electrónicos. Dado que las capturas están destinadas a manejar circunstancias excepcionales y finalmente deben ejecutarse, estar a salvo y declarar que este es un problema en el momento de la compilación es mucho mejor que en el tiempo de ejecución.

John Christensen
fuente
55

¿Cómo podría estar seguro de que alcanzó la parte de declaración en su bloque de captura? ¿Qué pasa si la instanciación arroja la excepción?

Burkhard
fuente
66
¿Eh? Las declaraciones de variables no arrojan excepciones.
Joshua
66
De acuerdo, es la instanciación que podría lanzar la excepción.
Burkhard el
19

Tradicionalmente, en lenguajes de estilo C, lo que sucede dentro de las llaves se mantiene dentro de las llaves. Creo que tener la vida útil de un tramo variable en ámbitos como ese no sería intuitivo para la mayoría de los programadores. Puede lograr lo que desea al encerrar los bloques try / catch / finalmente dentro de otro nivel de llaves. p.ej

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

EDIT: Creo que todas las reglas no tienen una excepción. Lo siguiente es válido C ++:

int f() { return 0; }

void main() 
{
    int y = 0;

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

El alcance de x es el condicional, la cláusula then y la cláusula else.

Ferruccio
fuente
10

Todos los demás han mencionado lo básico: lo que sucede en un bloque permanece en un bloque. Pero en el caso de .NET, puede ser útil examinar lo que el compilador cree que está sucediendo. Tome, por ejemplo, el siguiente código try / catch (tenga en cuenta que StreamReader se declara, correctamente, fuera de los bloques):

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();
        }
    }
}

Esto compilará algo similar a lo siguiente en 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

Que vemos MSIL respeta los bloques: son intrínsecamente parte del código subyacente generado cuando compila su C #. El alcance no solo está establecido en la especificación C #, sino también en las especificaciones CLR y CLS.

El alcance lo protege, pero ocasionalmente debe evitarlo. Con el tiempo, te acostumbras y comienza a sentirse natural. Como todos los demás dijeron, lo que sucede en un bloque permanece en ese bloque. ¿Quieres compartir algo? Tienes que salir de los bloques ...

John Rudy
fuente
8

En cualquier caso, en C ++, el alcance de una variable automática está limitado por las llaves que lo rodean. ¿Por qué alguien esperaría que esto fuera diferente al colocar una palabra clave de prueba fuera de las llaves?

Ravenspoint
fuente
1
Convenido; "}" significa fin de alcance. Sin embargo, try-catch-finally es inusual porque después de un bloque try, debe tener un bloqueo catch y / o finalmente; por lo tanto, una excepción a la regla normal donde el alcance de un bloque try llevado a la captura asociada / finalmente podría parecer aceptable?
Jon Schneider
7

Como señaló Ravenspoint, todos esperan que las variables sean locales para el bloque en el que están definidas. tryIntroduce un bloque y tambiéncatch .

Si desea variables locales para ambos tryy catch, intente encerrar ambos en un bloque:

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

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

La respuesta simple es que C y la mayoría de los lenguajes que han heredado su sintaxis tienen un alcance de bloque. Eso significa que si una variable se define en un bloque, es decir, dentro de {}, ese es su alcance.

La excepción, por cierto, es JavaScript, que tiene una sintaxis similar, pero tiene un alcance de función. En JavaScript, una variable declarada en un bloque try está dentro del alcance en el bloque catch, y en todas partes en su función de contención.

dgvid
fuente
4

@burkhard tiene la pregunta de por qué respondió correctamente, pero como una nota que quería agregar, aunque su ejemplo de solución recomendada es bueno 99.9999 +% de tiempo, no es una buena práctica, es mucho más seguro verificar si es nulo antes de usar algo se instancia dentro del bloque try, o inicializa la variable a algo en lugar de simplemente declararlo antes del bloque try. Por ejemplo:

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);
   }
}

Esto debería proporcionar escalabilidad en la solución alternativa, de modo que incluso cuando lo que está haciendo en el bloque try sea más complejo que asignar una cadena, debería poder acceder de manera segura a los datos desde su bloque catch.

Timothy Carter
fuente
4

De acuerdo con la sección titulada "Cómo lanzar y atrapar excepciones" en la Lección 2 del Kit de capacitación a su propio ritmo MCTS (Examen 70-536): Microsoft® .NET Framework 2.0 — Application Development Foundation , la razón es que la excepción puede haber ocurrido antes de las declaraciones de variables en el bloque try (como otros ya lo han notado).

Cita de la página 25:

"Observe que la declaración StreamReader se movió fuera del bloque Try en el ejemplo anterior. Esto es necesario porque el bloque Finalmente no puede acceder a las variables declaradas dentro del bloque Try. Esto tiene sentido porque dependiendo de dónde ocurrió una excepción, las declaraciones de variables dentro del Es posible que el bloque Try aún no se haya ejecutado ".

hurst
fuente
4

La respuesta, como todos han señalado, es más o menos "así es como se definen los bloques".

Hay algunas propuestas para hacer el código más bonito. Ver BRAZO

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

Se supone que los cierres también abordarán esto.

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

ACTUALIZACIÓN: ARM se implementa en Java 7. http://download.java.net/jdk7/docs/technotes/guides/language/try-with-resources.html

ykaganovich
fuente
2

Su solución es exactamente lo que debe hacer. No puede estar seguro de que su declaración se haya alcanzado incluso en el bloque try, lo que resultaría en otra excepción en el bloque catch.

Simplemente debe funcionar como ámbitos separados.

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
Masa en peligro de extinción
fuente
2

Las variables son de nivel de bloque y están restringidas a ese bloque Try o Catch. Similar a la definición de una variable en una declaración if. Piensa en esta situación.

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

La cadena nunca se declararía, por lo que no se puede depender de ella.

jW.
fuente
2

Porque el bloque try y el bloque catch son 2 bloques diferentes.

En el siguiente código, ¿esperaría que los definidos en el bloque A sean visibles en el bloque B?

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

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

Si bien en su ejemplo es extraño que no funcione, tome este similar:

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

Esto provocaría que la captura arroje una excepción de referencia nula si se rompe el Código 1. Ahora, si bien la semántica de try / catch se entiende bastante bien, este sería un caso de esquina molesto, ya que s se define con un valor inicial, por lo que en teoría nunca debería ser nulo, pero bajo una semántica compartida, lo sería.

Nuevamente, esto en teoría podría solucionarse permitiendo solo definiciones separadas ( String s; s = "1|2";), o algún otro conjunto de condiciones, pero en general es más fácil decir simplemente no.

Además, permite que la semántica del alcance se defina globalmente sin excepción, específicamente, los locales duran tanto como {}se definen en todos los casos. Punto menor, pero un punto.

Finalmente, para hacer lo que quiera, puede agregar un conjunto de corchetes alrededor de la captura de prueba. Le brinda el alcance que desea, aunque tiene el costo de una pequeña legibilidad, pero no demasiado.

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

En el ejemplo específico que ha dado, inicializar s no puede generar una excepción. Entonces pensarías que tal vez su alcance podría extenderse.

Pero en general, las expresiones de inicializador pueden arrojar excepciones. No tendría sentido para una variable cuyo inicializador arrojó una excepción (o que fue declarada después de otra variable donde sucedió eso) para estar en el alcance de catch / finalmente.

Además, la legibilidad del código sufriría. La regla en C (y los lenguajes que la siguen, incluidos C ++, Java y C #) es simple: los ámbitos variables siguen a los bloques.

Si desea que una variable esté dentro del alcance de try / catch / finally pero en ningún otro lugar, envuelva todo en otro conjunto de llaves (un bloque desnudo) y declare la variable antes del intento.

Steve Jessop
fuente
1

Parte de la razón por la que no están en el mismo alcance es porque en cualquier punto del bloque try, puede haber lanzado la excepción. Si estuvieran en el mismo alcance, sería un desastre esperar, porque dependiendo de dónde se lanzara la excepción, podría ser aún más ambiguo.

Al menos cuando se declara fuera del bloque try, usted sabe con seguridad cuál podría ser la variable como mínimo cuando se lanza una excepción; El valor de la variable antes del bloque try.

zxcv
fuente
1

Cuando declara una variable local, se coloca en la pila (para algunos tipos, el valor completo del objeto estará en la pila, para otros tipos, solo una referencia estará en la pila). Cuando hay una excepción dentro de un bloque try, las variables locales dentro del bloque se liberan, lo que significa que la pila se "desenrolla" al estado en que estaba al principio del bloque try. Esto es por diseño. Así es como el try / catch puede retroceder todas las llamadas de función dentro del bloque y vuelve a poner su sistema en un estado funcional. Sin este mecanismo, nunca podría estar seguro del estado de nada cuando se produce una excepción.

Hacer que su código de manejo de errores se base en variables declaradas externamente que tienen sus valores cambiados dentro del bloque try me parece un mal diseño. Lo que está haciendo es esencialmente filtrar recursos intencionalmente para obtener información (en este caso particular, no es tan malo porque solo está filtrando información, pero ¿se imagina si se tratara de algún otro recurso? futuro). Sugeriría dividir sus bloques de prueba en fragmentos más pequeños si necesita más granularidad en el manejo de errores.

Cuña
fuente
1

Cuando tiene un intento de captura, en su mayor parte debe saber los errores que podría arrojar. Estas clases de excepción normalmente dicen todo lo que necesita sobre la excepción. Si no, debe hacer sus propias clases de excepción y pasar esa información. De esa forma, nunca necesitará obtener las variables desde el interior del bloque try, porque la excepción se explica por sí misma. Entonces, si necesita hacer esto mucho, piense en su diseño e intente pensar si hay alguna otra manera, que puede predecir las próximas excepciones o utilizar la información proveniente de las excepciones, y luego tal vez volver a lanzar la suya. excepción con más información.

Jesper Blad Jensen
fuente
1

Como han señalado otros usuarios, las llaves definen el alcance en casi todos los lenguajes de estilo C que conozco.

Si es una variable simple, ¿por qué te importa cuánto tiempo estará dentro del alcance? No es gran cosa.

en C #, si es una variable compleja, querrá implementar IDisposable. Luego puede usar try / catch / finally y llamar a obj.Dispose () en el bloque finally. O puede usar la palabra clave using, que llamará automáticamente a Dispose al final de la sección de código.

Charles Graham
fuente
1

En Python son visibles en los bloques catch / finalmente si la línea que los declara no se lanzó.


fuente
1

¿Qué pasa si la excepción se produce en algún código que está por encima de la declaración de la variable? Lo que significa que la declaración en sí no se realizó en este caso.

try {

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

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

La especificación C # (15.2) establece que "el alcance de una variable local o constante declarada en un bloque es el bloque".

(en su primer ejemplo, el bloque try es el bloque donde se declara "s")

Tamberg
fuente
0

Mi pensamiento sería que debido a que algo en el bloque try activó la excepción, no se puede confiar en su contenido del espacio de nombres, es decir, hacer referencia a la cadena 's' en el bloque catch podría provocar el lanzamiento de otra excepción.

jpbarto
fuente
0

Bueno, si no arroja un error de compilación, y podría declararlo para el resto del método, entonces no habría forma de declararlo solo dentro del alcance de prueba. Te obliga a ser explícito sobre dónde se supone que existe la variable y no hace suposiciones.

kemiller2002
fuente
0

Si ignoramos el problema del bloque de alcance por un momento, el cumplidor tendría que trabajar mucho más en una situación que no está bien definida. Si bien esto no es imposible, el error de alcance también lo obliga a usted, el autor del código, a darse cuenta de la implicación del código que escribe (que la cadena s puede ser nula en el bloque catch). Si su código era legal, en el caso de una excepción OutOfMemory, ni siquiera se garantiza que se asigne una ranura de memoria:

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

El CLR (y, por lo tanto, el compilador) también lo obliga a inicializar las variables antes de que se usen. En el bloque catch presentado no puede garantizar esto.

Así que terminamos con el compilador teniendo que hacer mucho trabajo, lo que en la práctica no proporciona muchos beneficios y probablemente confundiría a las personas y los llevaría a preguntar por qué try / catch funciona de manera diferente.

Además de la coherencia, al no permitir nada elegante y adherirse a la semántica de alcance ya establecida utilizada en todo el lenguaje, el compilador y CLR pueden proporcionar una mayor garantía del estado de una variable dentro de un bloque catch. Que existe y se ha inicializado.

Tenga en cuenta que los diseñadores de idiomas han hecho un buen trabajo con otras construcciones como usar y bloquear donde el problema y el alcance están bien definidos, lo que le permite escribir código más claro.

por ejemplo, la palabra clave using con objetos IDisposable en:

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

es equivalente a:

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

Si su intento / captura / finalmente es difícil de entender, intente refactorizar o introducir otra capa de indirección con una clase intermedia que encapsule la semántica de lo que está tratando de lograr. Sin ver el código real, es difícil ser más específico.

Robert Paulson
fuente
0

En lugar de una variable local, se podría declarar una propiedad pública; Esto también debería evitar otro error potencial de una variable no asignada. cadena pública S {get; conjunto; }

útilBee
fuente
-1

Si la operación de asignación falla, su instrucción catch tendrá una referencia nula a la variable no asignada.

Desarrollador SaaS
fuente
2
No está asignado. Ni siquiera es nulo (a diferencia de la instancia y las variables estáticas).
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;
})();
núcleo
fuente
WTF? ¿Por qué el voto negativo? La encapsulación es parte integral de OOP. Se ve bonita también.
núcleo
2
No fui el voto negativo, pero lo que está mal es devolver una cadena no inicializada.
Ben Voigt