为什么在“catch”或“finally”范围内的“try”中没有声明变量?

时间:2008-09-18 17:56:24

标签: c# java exception scope language-design

在C#和Java(以及可能的其他语言)中,在“try”块中声明的变量不在相应的“catch”或“finally”块的范围内。例如,以下代码无法编译:

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

在此代码中,catch块中对s的引用发生编译时错误,因为s仅在try块的范围内。 (在Java中,编译错误是“s无法解决”;在C#中,它是“当前上下文中不存在名称”。)

这个问题的一般解决方案似乎是在try块之前而不是在try块中声明变量:

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

然而,至少对我来说,(1)这感觉就像一个笨重的解决方案,并且(2)它导致变量具有比程序员预期更大的范围(整个方法的其余部分,而不仅仅是在try-catch-finally的上下文。

我的问题是,这种语言设计决策(使用Java,C#和/或任何其他适用语言)背后的理由是什么?

28 个答案:

答案 0 :(得分:156)

两件事:

  1. 通常,Java只有两个级别的范围:全局和功能。但是,try / catch是一个例外(没有双关语)。抛出异常并且异常对象获得分配给它的变量时,该对象变量仅在“catch”部分中可用,并且一旦捕获完成就会被销毁。

  2. (更重要的是)。您无法知道try块中抛出异常的位置。它可能在您的变量被声明之前。因此,无法说出catch / finally子句可用的变量。考虑以下情况,其中范围如您所建议:

    
    try
    {
        throw new ArgumentException("some operation that throws an exception");
        string s = "blah";
    }
    catch (e as ArgumentException)
    {  
        Console.Out.WriteLine(s);
    }
    
  3. 这显然是个问题 - 当你到达异常处理程序时,s将不会被声明。鉴于捕获是为了处理特殊情况并最终必须执行,安全并在编译时声明这个问题远比在运行时更好。

答案 1 :(得分:55)

你怎么能确定你到达了拦截区的声明部分?如果实例化抛出异常怎么办?

答案 2 :(得分:17)

传统上,在C风格的语言中,花括号内部发生的事情会停留在花括号内。我认为在这样的范围内拥有变量的生命周期对大多数程序员来说都是不直观的。你可以通过将try / catch / finally块包含在另一个括号内来实现你想要的。 e.g。

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

编辑:我猜每个规则有例外。以下是有效的C ++:

int f() { return 0; }

void main() 
{
    int y = 0;

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

x的范围是conditional,then子句和else子句。

答案 3 :(得分:9)

其他人都提出了基础知识 - 块中发生的事情会停留在块中。但就.NET而言,检查编译器认为发生了什么可能会有所帮助。举例来说,下面的try / catch代码(请注意,StreamReader是在块之外正确声明的):

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

这将在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

我们看到了什么? MSIL尊重这些块 - 它们本质上是编译C#时生成的底层代码的一部分。范围不仅仅是C#规范中的硬件设置,它也在CLR和CLS规范中。

范围可以保护您,但您偶尔也需要解决它。随着时间的推移,你会习惯它,并开始感到自然。像其他人说的那样,块中发生的事情会停留在该块中。你想分享一些东西吗?你必须走出街区......

答案 4 :(得分:8)

无论如何,在C ++中,自动变量的范围受到它周围的花括号的限制。为什么有人会通过在大括号外面删除一个try关键字来预期这会有所不同?

答案 5 :(得分:5)

简单的答案是C和大多数继承了其语法的语言都是块作用域的。这意味着如果变量是在一个块中定义的,即在{}内,那就是它的范围。

顺便说一下,异常是JavaScript,它具有类似的语法,但是功能范围。在JavaScript中,try块中声明的变量在catch块的范围内,以及其包含函数中的其他位置。

答案 6 :(得分:5)

就像ravenspoint所指出的那样,每个人都希望变量在它们定义的块中是局部的。try引入了一个块,catch也是如此。

如果您想要trycatch的本地变量,请尝试将两者都包含在一个块中:

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

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

答案 7 :(得分:4)

正如大家所指出的那样,答案几乎就是“这就是块的定义方式”。

有一些建议可以使代码更漂亮。见ARM

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

Closures也应该解决这个问题。

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

更新: ARM在Java 7中实现。http://download.java.net/jdk7/docs/technotes/guides/language/try-with-resources.html

答案 8 :(得分:4)

@burkhard有一个问题,为什么回答得当,但作为一个说明我想补充一下,虽然你推荐的解决方案例子好99.9999 +%的时间,但这不是一个好的做法,要么检查更安全在使用try块中的实例化之前使用null,或者将变量初始化为某些内容而不是仅在try块之前声明它。例如:

string s = String.Empty;
try
{
    //do work
}
catch
{
   //safely access s
   Console.WriteLine(s);
}

或者:

string s;
try
{
    //do work
}
catch
{
   if (!String.IsNullOrEmpty(s))
   {
       //safely access s
       Console.WriteLine(s);
   }
}

这应该在变通方法中提供可伸缩性,因此即使您在try块中执行的操作比分配字符串更复杂,您也应该能够安全地从catch块中访问数据。

答案 9 :(得分:3)

根据 MCTS Self-Paced Training Kit(考试70-536)第2课中标题为“如何投掷和捕获例外”的部分:Microsoft®.NETFramework 2.0-应用程序开发基础原因是在try块中的变量声明之前可能发生了异常(正如其他人已经注意到的那样)。

第25页的引用:

“请注意,在前面的示例中,StreamReader声明已移出Try块。这是必要的,因为Finally块无法访问在Try块中声明的变量。 这是有道理的,因为取决于发生异常的地方,Try块中的变量声明可能尚未执行 。“

答案 10 :(得分:2)

因为try块和catch块是2个不同的块。

在下面的代码中,您是否希望在块B中可以看到块A中定义的s?

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

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

答案 11 :(得分:2)

变量是块级别,仅限于Try或Catch块。类似于在if语句中定义变量。想想这种情况。

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

永远不会声明String,因此不能依赖它。

答案 12 :(得分:2)

你的解决方案正是你应该做的。您无法确定是否在try块中达到了您的声明,这将导致catch块中的另一个异常。

它必须作为单独的范围工作。

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

答案 13 :(得分:1)

如果在某些代码中抛出异常,该代码高于变量的声明。这意味着,在这种情况下,声明本身并未发生。

try {

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

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

答案 14 :(得分:1)

它们不在同一范围内的部分原因是因为在try块的任何位置,您都可以抛出异常。如果它们处于相同的范围内,那么它就是等待的灾难,因为根据异常的抛出位置,它可能更加模糊。

至少当它在try块之外声明时,你肯定知道在抛出异常时最小的变量是什么; try块之前的变量值。

答案 15 :(得分:1)

在您给出的具体示例中,初始化s不能抛出异常。所以你认为它的范围可能会延长。

但一般来说,初始化表达式可以抛出异常。对于一个初始化器抛出异常(或者在发生的另一个变量之后声明的变量)在catch / finally范围内的变量没有意义。

此外,代码可读性会受到影响。 C中的规则(及其后面的语言,包括C ++,Java和C#)很简单:变量范围跟随块。

如果你想让变量在try / catch / finally的范围内,而不是其他地方,那么将整个东西包装在另一组括号中(一个裸块)并在try之前声明变量。

答案 16 :(得分:1)

虽然在你的例子中它不起作用很奇怪,但是采取类似的方法:

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

如果代码1中断,这将导致catch抛出空引用异常。现在虽然很好地理解了try / catch的语义,但这将是一个烦人的极端情况,因为s是用初始值定义的,所以理论上它应该永远不会为null,但在共享语义下,它将是。

理论上,这可以通过仅允许分离的定义(String s; s = "1|2";)或其他一些条件来解决,但通常更容易说不。

此外,它允许全局定义范围的语义,毫无例外,具体而言,在所有情况下,本地人只要定义它们{}就会持续。小点,但有一点。

最后,为了做你想做的事,你可以在try catch周围添加一组括号。给你你想要的范围,虽然它的确需要一点可读性,但不是太多。

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

答案 17 :(得分:1)

当你声明一个局部变量时,它被放置在堆栈上(对于某些类型,对象的整个值将在堆栈上,对于其他类型,只有一个引用将在堆栈上)。当try块内部存在异常时,块中的局部变量被释放,这意味着堆栈被“展开”回到try块开始时的状态。这是设计的。这就是try / catch能够退出块内所有函数调用并使系统恢复功能状态的方式。如果没有这种机制,你就无法确定发生异常时的状态。

让您的错误处理代码依赖于外部声明的变量,这些变量在try块中的值发生变化对我来说似乎是糟糕的设计。你正在做的事情本质上是为了获取信息而故意泄漏资源(在这种特殊情况下它并不是那么糟糕,因为你只是泄漏信息,但想象一下,如果它是其他资源?你只是让自己的生活变得更加困难未来)。如果您在错误处理方面需要更多粒度,我建议将您的try块拆分为更小的块。

答案 18 :(得分:1)

在Python中,如果声明它们的行没有抛出,它们在catch / finally块中可见。

答案 19 :(得分:1)

当你尝试捕获时,你应该知道它可能抛出的错误。 Theese Exception类正常地告诉你需要关于异常的所有内容。如果没有,你应该让你自己的异常类并传递这些信息。这样,你永远不需要从try块中获取变量,因为Exception是自我解释的。因此,如果您需要这么做,请考虑您的设计,并尝试思考是否有其他方式,您可以预测异常提交,或使用来自异常的信息,然后可能重新抛出自己的有更多信息的例外。

答案 20 :(得分:1)

正如其他用户所指出的那样,大括号在我所知道的几乎所有C风格的语言中都定义了范围。

如果它是一个简单的变量,那你为什么要关心它的范围有多长?这不是什么大不了的事。

在C#中,如果它是一个复杂的变量,你将需要实现IDisposable。然后,您可以使用try / catch / finally并在finally块中调用obj.Dispose()。或者您可以使用using关键字,它将自动调用代码部分末尾的Dispose。

答案 21 :(得分:0)

可以声明公共属性,而不是局部变量;这也应该避免未分配变量的另一个潜在错误。 公共字符串S {get;组; }

答案 22 :(得分:0)

如果我们暂时忽略范围阻塞问题,那么编译器必须在一个没有明确定义的情况下更加努力。虽然这不是不可能的,但是范围错误也迫使您(代码的作者)意识到您编写的代码的含义(字符串s在catch块中可能为null)。如果您的代码是合法的,在OutOfMemory异常的情况下,甚至不能保证s被分配一个内存插槽:

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

CLR(以及编译器)也会强制您在使用变量之前对其进行初始化。在提出的catch块中,它无法保证这一点。

因此,我们最终会让编译器不得不做很多工作,这在实践中并没有带来太多好处,可能会让人感到困惑并引导他们问为什么try / catch的工作方式不同。

除了一致性之外,通过不允许任何花哨和遵守已经建立的整个语言中使用的作用域语义,编译器和CLR能够更好地保证catch块内变量的状态。它存在并已初始化。

请注意,语言设计人员已经完成了其他构造的良好工作,例如使用 lock ,其中问题和范围已明确定义,这使您可以编写更清晰的代码

e.g。 使用关键字与 IDisposable 对象:

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

相当于:

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

如果您的try / catch / finally很难理解,请尝试重构或引入另一层间接,并使用一个中间类来封装您尝试完成的语义。如果没有真正的代码,就很难具体了。

答案 23 :(得分:0)

C# Spec(15.2)声明“块中声明的局部变量或常量的范围是块。”

(在你的第一个例子中,try块是声明“s”的块)

答案 24 :(得分:0)

如果它没有抛出编译错误,并且你可以为方法的其余部分声明它,那么就没有办法只在try范围内声明它。它强迫你明确知道变量应该存在的位置,而不是假设。

答案 25 :(得分:0)

我的想法是因为try块中的某些东西触发了异常,它的命名空间内容不能被信任 - 即在catch块中引用String的'可能会引发另一个异常。

答案 26 :(得分:-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;
})();

答案 27 :(得分:-1)

如果赋值操作失败,则catch语句将返回未赋值变量的空引用。