为什么在"捕获"或者"最终"范围内的"尝试"中未声明变量?

时间:2020-03-06 14:22:37  来源:igfitidea点击:

在Java的Cand(可能还有其他语言)中,在" 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#和/或者任何其他适用的语言)背后的基本原理是什么?

解决方案

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

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

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

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

解决方案正是我们应该做的。我们不能确保在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

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

两件事情:

  • 通常,Java只有两个级别的范围:全局和功能。但是,try / catch是一个例外(无双关语)。当引发异常并且异常对象获得分配给它的变量时,该对象变量仅在"捕获"部分中可用,并在捕获完成后立即销毁。
  • (更重要的是)。我们不知道在try块中的何处引发了异常。可能是在声明变量之前。因此,无法说出哪些变量可用于catch / finally子句。考虑以下情况,如我们所建议的那样进行范围界定:
try
{
    throw new ArgumentException("some operation that throws an exception");
    string s = "blah";
}
catch (e as ArgumentException)
{  
    Console.Out.WriteLine(s);
}

当我们到达异常处理程序时,这显然是个问题,它将不会被声明。鉴于catch是为了处理特殊情况而必须最终执行,因此安全并在编译时声明一个问题比在运行时要好得多。

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

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

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

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

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

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

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

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

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

如果我们想让变量既在try变量又在catch变量中,请尝试将它们都放在一个块中:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

CSpec(15.2)声明"在块中声明的局部变量或者常量的范围与该块相同。"

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

问候,
坦贝格

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

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

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

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

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

有一些建议可以使代码更漂亮。见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

其他所有人都提出了基础知识-区块中发生的一切都停留在区块中。但是对于.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#时生成的基础代码的一部分。作用域不仅在Cspec中是硬性设置的,在CLR和CLS规范中也是如此。

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

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

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

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

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

引用第25页:

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

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

try {

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

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