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

关于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语句将具有返回未赋值变量的空引用。


    推荐阅读

      linux启动r语言命令?

      linux启动r语言命令?,数据,环境,官网,服务,系统,时间,电脑,统一,软件,语言,li

      linux常用命令c语言?

      linux常用命令c语言?,系统,工作,信息,管理,基础,命令,地址,目录,简介,时间,li

      linux命令行设置语言?

      linux命令行设置语言?,系统,管理,环境,国家,工具,电脑,软件,文化,底部,语言,l

      linux使用命令改语言?

      linux使用命令改语言?,系统,工作,管理,电脑,设备,字符集,中文,命令,语言,虚

      linux变量释放命令?

      linux变量释放命令?,系统,环境,名称,工具,官网,简介,变量,环境变量,命令,内

      c语言写linux命令?

      c语言写linux命令?,系统,工具,代码,智能,工作,环境,情况,位置,命令,文件,如何

      linux汇编语言命令?

      linux汇编语言命令?,系统,地址,代码,数据,网络,平台,平均,位置,灵活,工作,汇

      linux汇编语言命令?

      linux汇编语言命令?,系统,地址,代码,数据,网络,平台,平均,位置,灵活,工作,汇

      linux调出变量的命令?

      linux调出变量的命令?,系统,工作,工具,信息,地址,代码,标准,名称,官网,命令,l

      linux命令是什么语言?

      linux命令是什么语言?,系统,环境,代码,传播,管理,语言,操作系统,源码,自由,

      linux命令主机名变量?

      linux命令主机名变量?,系统,主机名,查询系统,命令,终端,编辑,提示符,根目

      linux改语言命令行?

      linux改语言命令行?,系统,环境,工具,密码,概念,地方,软件,通信,管理,国际,lin

      linux命令行c语言?

      linux命令行c语言?,代码,系统,工具,环境,工作,保险,发行,命令,文件,终端,linu

      c语言在linux命令?

      c语言在linux命令?,系统,工作,管理,命令,保险,基础,环境,信息,文件,语言,linu

      linux编写c语言命令?

      linux编写c语言命令?,系统,基础,环境,代码,盘面,保险,百度,情况,数据,工具,在

      linux变量是一个命令?

      linux变量是一个命令?,系统,信息,变量,名称,官网,地址,环境,代码,地方,命令,$

      linux中变量取余命令?

      linux中变量取余命令?,地址,工作,系统,数据,信息,命令,分析,目录,控制台,文

      加载变量的linux命令?

      加载变量的linux命令?,工具,系统,名称,环境变量,环境,命令,用户,文件,变量,

      linux改变语言命令?

      linux改变语言命令?,系统,管理,网上,官方网站,情况,服务,中文,语言,命令,终

      c语言编译linux命令?

      c语言编译linux命令?,代码,工具,环境,系统,基础,保险,百度,语言,源程序,文件