为什么不在“捕获”或“最终”范围内的“尝试”中声明变量?


139

在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中,编译错误是“无法解决”;在C#中,它是“名称s在当前上下文中不存在”。)

解决此问题的一般方法似乎是在try块之前而不是在try块内声明变量:

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

但是,至少对我来说,(1)感觉像一个笨拙的解决方案,(2)它导致变量的范围比程序员预期的范围大(方法的整个其余部分,而不仅仅是在上下文中)。最终尝试捕获)。

我的问题是,此语言设计决定(使用Java,C#和/或任何其他适用的语言)背后的基本原理是什么?

Answers:


171

两件事情:

  1. 通常,Java只有两个级别的范围:全局和功能。但是,try / 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旨在处理特殊情况,并且最终必须执行,因此安全并在编译时声明一个问题比在运行时要好得多。


55

您如何确定已到达catch块中的声明部分?如果实例化引发异常怎么办?


6
??变量声明不会引发异常。
约书亚

6
同意的是可能引发异常的实例化。
Burkhard

19

传统上,在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子句。


10

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

示波器可以保护您,但是您有时需要解决它。随着时间的流逝,您已经习惯了,它开始变得自然。就像其他人说的那样,一个区块中发生的一切仍然存在。你想分享一些东西吗?您必须走出街区...


8

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


1
同意;“}”表示作用域的结尾。但是,try-catch-finally最终是不寻常的,因为在try块之后,您必须具有catch和/或finally块;因此,一个正常规则的例外情况似乎可以接受吗?
乔恩·施耐德

7

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

如果希望变量对于try和都是局部的catch,请尝试将它们都包含在一个块中:

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

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

5

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

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


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块访问数据。


4

根据MCTS自定进度培训工具包(考试70-536)第2课中标题为“如何引发和捕获异常”的部分:Microsoft®.NET Framework 2.0-Application Development Foundation,原因是可能发生了异常在try块中的变量声明之前(其他人已经注意到)。

引用第25页:

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


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


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

2

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

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

永远不会声明字符串,因此不能依赖它。


2

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

在以下代码中,您希望在块A中定义的s在块B中可见吗?

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

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

2

虽然在您的示例中,它不起作用很奇怪,但请采取以下类似方法:

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

如果代码1中断,这将导致catch引发空引用异常。现在,虽然对try / catch的语义有了很好的理解,但这将是一个令人烦恼的特殊情况,因为s是用初始值定义的,因此从理论上讲它永远不应为null,但在共享语义下,它将为null。

同样,从理论上讲,可以只允许使用单独的定义(String s; s = "1|2";)或其他一些条件来解决此问题,但通常只说不就容易了。

另外,它允许范围的语义无一例外地全局定义,特别是本地语言只要 {}是在所有情况下,定义它们存在。小一点,但一点。

最后,为了执行您想要的操作,可以在try catch周围添加一组括号。为您提供所需的范围,尽管它的确增加了一些可读性,但又不过分。

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

1

在您提供的特定示例中,初始化s不会引发异常。因此,您可能认为它的范围可能会扩展。

但是通常,初始化器表达式可以引发异常。对于一个其初始化程序引发异常(或在发生该事件的另一个变量之后声明的异常)的变量而言,将其包含在catch / final范围内是没有意义的。

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

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


1

它们不在同一范围内的部分原因是因为在try块的任何时候都可能引发异常。如果它们处于相同的范围内,那么等待就很麻烦,因为根据抛出异常的位置,它可能会更加模棱两可。

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


1

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

让您的错误处理代码依赖于外部声明的变量,这些变量的值在try块内更改,这对我来说似乎是不好的设计。您所做的本质上是有意地泄漏资源以获取信息(在这种情况下,这并不是很糟糕,因为您只是泄漏信息,但想像一下是否还有其他资源吗?未来)。如果您需要更多粒度的错误处理,建议将try块分解为较小的块。


1

尝试捕获时,您最多应该知道它可能会引发的错误。Theese Exception类通常告诉您有关该异常的所有信息。如果不是,则应使自己成为异常类,并传递这些信息。这样,您将永远不需要从try块中获取变量,因为Exception是自我解释的。因此,如果您需要大量进行此操作,请考虑一下自己的设计,然后尝试考虑是否有其他方法可以预测异常的到来,或者使用异常产生的信息,然后重新抛出自己的信息。有更多信息的例外。


1

正如其他用户所指出的那样,花括号几乎用我所知道的每种C样式语言来定义范围。

如果它是一个简单的变量,那么为什么还要关心它在范围内会持续多久?没什么大不了的。

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


1

在Python中,如果声明未抛出异常的行,则它们在catch / finally块中可见。


1

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

try {

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

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

1

C#规格(15.2)指出“局部变量或常数的范围中的IST块的块宣布”。

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


0

我的想法是,由于try块中的某些内容触发了异常,因此其命名空间内容无法被信任-即,在catch块中引用String可能导致引发另一个异常。


0

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


0

如果我们暂时忽略范围限制问题,那么在未明确定义的情况下,编译器将不得不更加努力地工作。尽管这并非不可能,但范围界定错误也迫使您(代码的创建者)意识到所编写代码的含义(在catch块中字符串s可能为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很难理解,请尝试使用中间类重构或引入另一层间接方法,该中间类封装了您要完成的工作的语义。如果不看真实的代码,很难更具体。


0

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



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

WTF?为什么要下票?封装是OOP不可或缺的。看起来也不错
核心

2
我不是不好的人,但出问题的是返回未初始化的字符串。
Ben Voigt 2010年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.