157

在 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 cannot be resolved”;在 C# 中,是“The name 's' does not exist in the current context”。)

这个问题的一般解决方案似乎是在 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# 和/或任何其他适用的语言中)?

4

28 回答 28

186

两件事情:

  1. 一般来说,Java 只有 2 个级别的范围:全局和函数。但是,try/catch 是一个例外(没有双关语的意思)。当抛出异常并且异常对象获得分配给它的变量时,该对象变量仅在“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);
    }
    

这显然是一个问题——当您到达异常处理程序时,s 将不会被声明。鉴于 catch 旨在处理异常情况并且 finally必须执行,因此在编译时安全并声明这是一个问题比在运行时要好得多。

于 2008-09-18T18:01:57.237 回答
55

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

于 2008-09-18T17:58:40.020 回答
22

传统上,在 C 风格的语言中,花括号内发生的事情都保留在花括号内。我认为让变量的生命周期跨越这样的范围对于大多数程序员来说是不直观的。您可以通过将 try/catch/finally 块包含在另一层大括号中来实现您想要的。例如

... 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 的范围是条件、then 子句和 else 子句。

于 2008-09-18T18:03:22.177 回答
11

其他人都提出了基础知识——一个块中发生的事情留在一个块中。但在 .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 规范中也是如此。

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

于 2008-09-18T21:48:08.067 回答
9

无论如何,在 C++ 中,自动变量的范围受到围绕它的花括号的限制。为什么有人会期望通过在花括号外插入一个 try 关键字来实现这一点?

于 2008-09-18T17:59:19.350 回答
7

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

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

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

        throw new Exception(":(")
    }
    catch (Exception e)
    {
        Debug.WriteLine(s);
    }
}
于 2008-09-18T18:03:58.853 回答
5

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

顺便说一句,JavaScript 是个例外,它有类似的语法,但它是函数作用域的。在 JavaScript 中,在 try 块中声明的变量在 catch 块的范围内,在其包含函数的其他任何地方。

于 2008-09-18T18:06:59.580 回答
5

根据MCTS Self-Paced Training Kit (Exam 70-536) 第 2 课:Microsoft® .NE​​T Framework 2.0—Application Development Foundation中标题为“如何抛出和捕获异常”的部分,原因是可能发生了异常在 try 块中的变量声明之前(正如其他人已经指出的那样)。

引自第 25 页:

“请注意,在前面的示例中,StreamReader 声明已移到 Try 块之外。这是必要的,因为 finally 块无法访问在 Try 块中声明的变量。这是有道理的,因为根据发生异常的位置,变量声明中的尝试块可能尚未执行。”

于 2008-09-19T07:31:29.193 回答
4

@burkhard 有一个关于为什么回答正确的问题,但作为我想添加的注释,虽然您推荐的解决方案示例在 99.9999+% 的时间内都很好,但这不是好的做法,在使用之前检查 null 会更安全在 try 块中实例化某个东西,或者将变量初始化为某个东西,而不是在 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 块访问数据。

于 2008-09-18T18:12:04.187 回答
4

正如每个人都指出的那样,答案几乎是“这就是定义块的方式”。

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

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

闭包也应该解决这个问题。

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

于 2008-09-18T18:37:26.747 回答
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
于 2008-09-18T18:00:22.840 回答
2

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

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

String 永远不会被声明,所以它不能被依赖。

于 2008-09-18T18:02:04.740 回答
2

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

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

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

{ // block B
  Console.Out.WriteLine(s); // or printf or whatever
}
于 2008-09-18T18:05:57.180 回答
2

虽然在您的示例中它不起作用很奇怪,但请使用类似的示例:

    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);
     }
}
于 2008-09-18T18:11:52.237 回答
2

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

于 2008-09-19T06:00:20.113 回答
1

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

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

此外,代码可读性也会受到影响。C(以及遵循它的语言,包括 C++、Java 和 C#)中的规则很简单:变量作用域遵循块。

如果您希望变量在 try/catch/finally 的范围内,但没有其他地方,则将整个事物包装在另一组大括号(裸块)中并在 try 之前声明变量。

于 2008-09-18T18:02:53.520 回答
1

它们不在同一范围内的部分原因是因为在 try 块的任何时候,您都可以抛出异常。如果它们在同一个范围内,等待是一场灾难,因为根据抛出异常的位置,它可能会更加模棱两可。

至少当它在 try 块之外声明时,您至少可以确定在抛出异常时该变量可能是什么;try 块之前的变量值。

于 2008-09-18T18:04:06.217 回答
1

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

让你的错误处理代码依赖于外部声明的变量,这些变量的值在 try 块内发生了变化,这对我来说似乎是糟糕的设计。您所做的实际上是为了获取信息而故意泄漏资源(在这种特殊情况下,这并没有那么糟糕,因为您只是在泄漏信息,但是想象一下它是否是其他资源?您只是在使自己的生活更加艰难未来)。如果您需要更精细的错误处理,我建议您将 try 块分成更小的块。

于 2008-09-18T18:12:41.627 回答
1

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

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

于 2008-09-18T18:14:59.060 回答
1

当你有一个 try catch 时,你应该最多知道它可能抛出的错误。Theese Exception 类通常会告诉您有关异常的所有信息。如果没有,您应该创建自己的异常类并传递该信息。这样,您将永远不需要从 try 块中获取变量,因为异常是自我解释的。因此,如果您需要做很多事情,请考虑一下您的设计,并尝试考虑是否有其他方式,您可以预测异常的到来,或者使用来自异常的信息,然后重新抛出您自己的有更多信息的例外。

于 2008-09-18T18:19:20.517 回答
1

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

如果它是一个简单的变量,那么您为什么要关心它在范围内的时间?这没什么大不了的。

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

于 2008-09-18T18:22:55.500 回答
1

如果在变量声明之上的某些代码中引发异常怎么办。这意味着,在这种情况下,声明本身并没有发生。

try {

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

} catch (Exception) {
        //Use s;//Problem here
} finally {
        //Use s;//Problem here
}
于 2010-12-29T10:23:14.093 回答
0

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

于 2008-09-18T17:58:42.657 回答
0

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

于 2008-09-18T17:58:58.490 回答
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 块内的变量状态提供更大的保证。它存在并已被初始化。

请注意,语言设计人员在其他构造方面做得很好,例如在问题和范围明确定义的地方使用锁定,这使您可以编写更清晰的代码。

例如IDisposable对象的using关键字:

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 难以理解,请尝试重构或引入另一层间接层,其中包含封装您尝试完成的语义的中间类。没有看到真实的代码,很难更具体。

于 2008-09-18T23:56:43.373 回答
0

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

于 2013-07-23T17:40:09.427 回答
-1

如果赋值操作失败,您的 catch 语句将对未赋值变量有一个空引用。

于 2008-09-18T18:01:18.907 回答
-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;
})();
于 2008-09-18T23:58:27.987 回答