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

Why aren't variables declared in “try” in scope in “catch” or “finally”?

在C语言和Java语言中(也可能是其他语言),在"尝试"块中声明的变量不在相应的"catch"或"最后"块中。例如,以下代码不编译:

1
2
3
4
5
6
7
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语言中,"名称S"不存在于当前上下文中。

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

1
2
3
4
5
6
7
8
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的上下文中)。

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


两件事:

  • 一般来说,Java只有2个级别的范围:全局和函数。但是,Try/Catch是一个例外(没有双关语)。当抛出异常并且异常对象得到一个分配给它的变量时,该对象变量只在"catch"部分中可用,并在catch完成后立即销毁。

  • (更重要的是)。您不知道在try块中的哪个位置引发了异常。它可能早于声明变量。因此,不可能说出catch/finally子句将使用哪些变量。考虑以下情况,其中范围界定如您建议的那样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    try
    {
        throw new ArgumentException("some operation that throws an exception");
        string s ="blah";
    }
    catch (e as ArgumentException)
    {  
        Console.Out.WriteLine(s);
    }
  • 这显然是一个问题-当您到达异常处理程序时,将不会声明S。考虑到捕获是为了处理异常情况,最终必须执行,因此安全性和在编译时声明这是一个问题要比在运行时好得多。


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


    传统上,在C样式语言中,大括号内的内容保持在大括号内。我认为,对于大多数程序员来说,在这样的范围内扩展变量的生命周期是没有意义的。通过将try/catch/finally块封闭在另一个大括号级别中,可以实现所需的功能。例如

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ... code ...
    {
        string s ="test";
        try
        {
            // more code
        }
        catch(...)
        {
            Console.Out.WriteLine(s);
        }
    }

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    int f() { return 0; }

    void main()
    {
        int y = 0;

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

    x的作用域是条件子句、then子句和else子句。


    其他人都提出了一些基本的问题——在一个块中发生的事情保持在一个块中。但是对于.NET,检查编译器认为正在发生的事情可能会有所帮助。以下面的Try/Catch代码为例(请注意,streamreader是在块外正确声明的):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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中的以下内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    .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规范中。

    范围保护您,但您偶尔也必须围绕它工作。随着时间的推移,你习惯了它,它开始感觉自然。就像其他人说的,在一个街区发生的事情就留在那个街区。你想分享一些东西吗?你必须走出街区…


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


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

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


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

    如果希望trycatch都是局部变量,请尝试将这两个变量都包含在一个块中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // here is some code
    {
        string s;
        try
        {

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

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

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

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

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

    1
    2
    3
    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下载


    @Burkhard有一个问题,为什么回答正确,但是作为我想补充的一个说明,虽然您推荐的解决方案示例是99.9999%的好时间,但这不是一个好的实践,在使用try块中的实例化之前检查空值,或者将变量初始化为某个值,而不只是声明它之前,这样做要安全得多。打开试块。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    string s = String.Empty;
    try
    {
        //do work
    }
    catch
    {
       //safely access s
       Console.WriteLine(s);
    }

    或:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    string s;
    try
    {
        //do work
    }
    catch
    {
       if (!String.IsNullOrEmpty(s))
       {
           //safely access s
           Console.WriteLine(s);
       }
    }

    这应该在解决方案中提供可伸缩性,这样即使在try块中所做的比分配字符串更复杂,您也应该能够从catch块安全地访问数据。


    根据MCTS自定步调培训工具包(考试70-536)第2课中题为"如何抛出和捕获异常"的部分:微软?.NETFramework 2.0应用程序开发基础,原因是异常可能发生在尝试块中的变量声明之前(正如其他人已经注意到的)。

    引自第25页:

    "请注意,在前面的示例中,streamreader声明被移到try块之外。这是必需的,因为finally块无法访问在try块中声明的变量。这很有意义,因为根据发生异常的位置,try块中的变量声明可能尚未执行。"


    你的解决方案正是你应该做的。您不能确定您的声明甚至在try块中被访问,这将在catch块中导致另一个异常。

    它必须作为单独的作用域工作。

    1
    2
    3
    4
    5
    6
    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

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

    1
    2
    3
    4
    5
    6
    try {    
        fileOpen("no real file Name");    
        String s ="GO TROJANS";
    } catch (Exception) {  
        print(s);
    }

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


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

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

    1
    2
    3
    4
    5
    6
    7
    { // block A
      string s ="dude";
    }

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

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

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


    它们不在同一范围的部分原因是,在try块的任何点上,都可以引发异常。如果它们在同一个范围内,那么等待将是一场灾难,因为根据抛出异常的位置,可能会更加模糊。

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


    当你有一个试捕获时,你最多应该知道它可能会抛出错误。这些异常类通常会告诉您关于异常所需的一切。如果不是,您应该使自己成为异常类并将该信息传递给其他人。这样,就不需要从try块内部获取变量,因为异常是可以自我解释的。所以如果你需要这么做,想想你的设计,试着想想如果有其他的方法,你可以预测异常合并,或者使用异常中的信息合并,然后用更多的信息重新处理你自己的异常。


    正如其他用户所指出的,大括号几乎定义了我所知道的每种C样式语言的作用域。

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

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


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

    但一般来说,初始化器表达式可以抛出异常。如果一个变量的初始化器抛出了一个异常(或在发生异常的另一个变量之后声明的异常),而该变量在catch/finally范围内,则这是不合理的。

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

    如果您希望某个变量在try/catch/finally的作用域内,但不在其他任何地方,那么请将整个变量包装在另一组大括号(裸块)中,并在try之前声明该变量。


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


    如果在变量声明上方的某些代码中抛出异常,该怎么办?也就是说,在这种情况下,声明本身并不是偶然发生的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    try {

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

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

    在您的示例中,它不起作用是很奇怪的,请看下面类似的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
        try
        {
             //Code 1
             String s ="1|2";
             //Code 2
        }
        catch
        {
             Console.WriteLine(s.Split('|')[1]);
        }

    如果代码1中断,这将导致catch引发空引用异常。现在,虽然try/catch的语义已经被很好地理解了,但这将是一个令人讨厌的角落案例,因为s是用初始值定义的,所以理论上它不应该是空的,但是在共享语义下,它应该是空的。

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

    此外,它允许在全局范围内定义作用域的语义,无例外,特别是,局部变量的持续时间只要在所有情况下定义它们的{}。小点,但一点。

    最后,为了做您想要做的事情,您可以在try catch周围添加一组括号。给你你想要的范围,尽管它以一点可读性为代价,但不要太多。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    {
         String s;
         try
         {
              s ="test";
              //More code
         }
         catch
         {
              Console.WriteLine(s);
         }
    }

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


    C规范(15.2)规定"在块中声明的局部变量或常量的范围是块的范围。"

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


    如果我们暂时忽略范围划分块的问题,编译器将不得不在定义不明确的情况下更加努力地工作。虽然这不是不可能的,但是范围错误也迫使代码的作者您认识到您所写代码的含义(catch块中的字符串S可能为空)。如果您的代码是合法的,在发生内存不足异常的情况下,甚至不能保证为S分配内存插槽:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // won't compile!
    try
    {
        VeryLargeArray v = new VeryLargeArray(TOO_BIG_CONSTANT); // throws OutOfMemoryException
        string s ="Help";
    }
    catch
    {
        Console.WriteLine(s); // whoops!
    }

    clr(因此编译器)还强制您在使用变量之前对其进行初始化。在呈现的捕获块中,它不能保证这一点。

    因此,我们最终得到的结果是编译器必须做大量的工作,在实践中,这并没有提供太多的好处,可能会使人们困惑,并导致他们问为什么try/catch的工作方式不同。

    除了一致性之外,通过不允许任何花哨的东西,并且坚持在整个语言中使用的已经建立的作用域语义,编译器和clr能够对catch块中的变量的状态提供更大的保证。它存在并且已经初始化。

    请注意,语言设计人员对其他构造(如在问题和范围定义良好的地方使用和锁定)做了很好的工作,这允许您编写更清晰的代码。

    例如,在以下位置使用带有IDisposable对象的using关键字:

    1
    2
    3
    4
    using(Writer writer = new Writer())
    {
        writer.Write("Hello");
    }

    相当于:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Writer writer = new Writer();
    try
    {        
        writer.Write("Hello");
    }
    finally
    {
        if( writer != null)
        {
            ((IDisposable)writer).Dispose();
        }
    }

    如果您的try/catch/finally很难理解,请尝试重构或引入另一个间接层,其中包含一个中间类,它封装了您要完成的工作的语义。如果没有看到真正的代码,就很难更加具体。


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


    我的想法是,因为try块中的某个东西触发了异常,所以它的命名空间内容不能被信任——即引用catch块中的字符串"s"可能导致引发另一个异常。


    C 3:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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;
    })();


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