为什么在“ catch”或“ finally”的作用域中的“ try”中没有声明变量?

在 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”不能被解析; 在 C # 中,它是“ The name‘ s’does not been 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 # 和/或任何其他适用的语言中)背后的基本原理是什么?

93004 次浏览

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

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

如果它没有抛出编译错误,并且您可以为方法的其余部分声明它,那么就没有办法只在 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 语句将返回对未赋值变量的 null 引用。

两件事:

  1. 一般来说,Java 只有两个作用域级别: 全局作用域和函数作用域。但是,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);
    }
    

This clearly is a problem - when you reach the exception handler, s will not have been declared. Given that catches are meant to handle exceptional circumstances and finallys must execute, being safe and declaring this a problem at compile time is far better than at runtime.

这些变量是块级别的,并且仅限于那个“尝试”或“捕捉”块。类似于在 if 语句中定义变量。想想这种情况。

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

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

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

但通常,初始化器表达式会抛出异常。如果一个变量的初始化器抛出了一个异常(或者这个异常在另一个变量之后声明) ,那么这个变量就没有意义了,因为它处于 catch/finally 的作用域中。

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

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

传统上,在 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也是如此。

如果希望变量同时局部于 trycatch,请尝试将这两个变量都封装在一个块中:

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


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

它们不在同一范围内的部分原因是,在 try 块的任何位置都可以抛出异常。如果它们在相同的范围内,那么等待就是一场灾难,因为这取决于抛出异常的位置,异常可能更加模糊。

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

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

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

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


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

简单的答案是,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,但是在共享语义下,它会为 null。

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

此外,它允许在全局范围内定义作用域的语义,特别是在所有情况下,只要在 {}中定义了局部变量,它们就会一直存在。小意思,但也是个意思。

最后,为了完成您想要的操作,您可以在 try catch 周围添加一组括号。给你你想要的范围,虽然它的代价是一点点可读性,但不是太多。

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

@ burkhard 有一个问题,为什么答案是正确的,但是作为一个注意,我想补充,虽然你推荐的解决方案的例子是好的99.9999 +% 的时间,这不是一个好的做法,它是更安全的,要么检查为空之前,使用尝试块中的东西实例化,或初始化变量,而不是只是声明它之前的 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 块分解成更小的块。

C # 规格(15.2)声明“在块中声明的局部变量或常量的作用域是块。”

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

当您有一个 try catch 时,您应该在大多数情况下知道它可能抛出的错误。这些 Exception 类通常告诉您关于异常所需的一切。如果没有,则应该创建自己的异常类并传递该信息。这样,您就不需要从 try 块中获取变量,因为 Exception 是自解释的。所以如果你需要经常这样做,想想你的设计,试着想想是否有其他的方法,你可以预测异常,或者使用来自异常的信息,然后可能用更多的信息重新抛出你自己的异常。

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

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

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

正如所有人都指出的那样,答案基本上是“这就是块的定义方式”。

有一些建议使代码更漂亮。参见 手臂

 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中实现的。 < a href = “ http://download.Java.net/jdk7/docs/techtes/guide/language/try-with-resources. html”rel = “ nofollow norefrer”> 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 # 时生成的底层代码的一部分。这个范围不仅在 C # 规范中是硬设置的,在 CLR 和 CLS 规范中也是如此。

瞄准镜能保护你,但你偶尔也得绕过去。随着时间的推移,你会习惯它,并开始感觉自然。就像其他人说的,一个街区里发生的事就留在那个街区里。有什么想说的吗?你必须走出街区。

如果我们暂时忽略范围块问题,那么在一个没有很好定义的情况下,编译器将不得不更加努力地工作。虽然这并非不可能,但范围错误也迫使代码的作者认识到所编写代码的含义(在 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 块中变量的状态。它存在并已初始化。

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

例如,使用关键字中的 一次性手机对象位于:

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 难以理解,可以尝试使用中间类重构或引入另一个间接层,该中间类封装了要完成的任务的语义。如果没有看到真正的代码,就很难说得更具体。

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

在 Python 中,如果声明它们的行没有抛出,那么在 catch/finally 块中可以看到它们。

根据 MCTS 自定速培训工具包(考试70-536) : Microsoft. NET Framework 2.0ー应用程序开发基金会第2课中题为“如何抛出和捕捉异常”的部分,原因是异常可能发生在 try 块中的变量声明之前(其他人已经注意到了)。

引自第25页:

”注意,在前面的示例中,StreamReader 声明被移到了 Tryblock 之外。这是必要的,因为 Finally 块不能访问在 Tryblock 中声明的变量。这是有意义的,因为根据异常发生的位置,可能还没有执行 Tryblock 中的变量声明

如果在某些代码中抛出异常,而这些代码位于变量声明之上,该怎么办。也就是说,声明本身并没有出现在这个案例中。

try {


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


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

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