为什么在“try”中声明的变量不在“catch”或“finally”的范围内?
-
01-07-2019 - |
题
在 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# 中,它是“当前上下文中不存在名称”。)
此问题的一般解决方案似乎是在 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# 和/或任何其他适用的语言中)?
解决方案
两件事情:
一般来说,Java 只有 2 个级别的作用域:全局和函数。但是,try/catch 是一个例外(没有双关语)。当抛出异常并且异常对象获得分配给它的变量时,该对象变量仅在“catch”部分中可用,并在 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); }
这显然是一个问题 - 当您到达异常处理程序时,s 将尚未被声明。鉴于捕获是为了处理特殊情况,最后 必须 执行,在编译时安全并声明这是一个问题比在运行时要好得多。
其他提示
您如何确定您已到达 catch 块中的声明部分?如果实例化抛出异常怎么办?
传统上,在 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 子句。
其他人都提出了基础知识——一个区块中发生的事情仍保留在一个区块中。但对于 .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 规范中也是如此。
范围可以保护您,但您有时确实需要解决它。随着时间的推移,你会习惯它,并且开始感觉很自然。就像其他人所说的那样,一个块中发生的事情将保留在该块中。你想分享一些东西吗?你必须走出街区......
无论如何,在 C++ 中,自动变量的范围受到围绕它的花括号的限制。为什么有人会期望通过在大括号之外插入 try 关键字来实现不同的结果呢?
就像 ravenspoint 指出的那样,每个人都希望变量对于定义它们的块来说是本地的。 try
引入了一个块,也是如此 catch
.
如果您想要两者的局部变量 try
和 catch
, ,尝试将两者都包含在一个块中:
// here is some code
{
string s;
try
{
throw new Exception(":(")
}
catch (Exception e)
{
Debug.WriteLine(s);
}
}
简单的答案是,C 和大多数继承其语法的语言都是块作用域的。这意味着如果一个变量是在一个块中定义的,即在 { } 内,那么这就是它的作用域。
顺便说一句,JavaScript 是个例外,它具有类似的语法,但具有函数作用域。在 JavaScript 中,try 块中声明的变量位于 catch 块的作用域内,并且位于其包含函数的其他任何位置。
@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 (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 课中标题为“如何抛出和捕获异常”的部分 MCTS 自定进度培训套件(考试 70-536):Microsoft® .NET Framework 2.0 — 应用程序开发基础, ,原因是异常可能发生在 try 块中的变量声明之前(正如其他人已经指出的那样)。
引自第25页:
“请注意,在前面的示例中,StreamReader 声明已移至 Try 块之外。这是必要的,因为 Final 块无法访问 Try 块中声明的变量。 这是有道理的,因为根据异常发生的位置,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
这些变量是块级的,并且仅限于 Try 或 Catch 块。类似于在 if 语句中定义变量。想想这种情况。
try {
fileOpen("no real file Name");
String s = "GO TROJANS";
} catch (Exception) {
print(s);
}
String 永远不会被声明,因此不能依赖它。
因为try块和catch块是2个不同的块。
在下面的代码中,您是否期望块 A 中定义的 s 在块 B 中可见?
{ // block A
string s = "dude";
}
{ // block B
Console.Out.WriteLine(s); // or printf or whatever
}
在您给出的具体示例中,初始化不能引发异常。所以你会认为它的范围也许可以扩大。
但一般来说,初始化表达式可能会引发异常。对于一个初始化程序引发异常的变量(或者在发生异常的另一个变量之后声明的变量)来说,在 catch/finally 的范围内是没有意义的。
此外,代码的可读性也会受到影响。C(以及遵循该规则的语言,包括 C++、Java 和 C#)中的规则很简单:变量作用域位于块之后。
如果您希望变量位于 try/catch/finally 的范围内,但不在其他地方,则将整个变量包装在另一组大括号(裸块)中,并在 try 之前声明该变量。
它们不在同一范围内的部分原因是因为在 try 块的任何点上,您都可能抛出异常。如果它们在同一范围内,那么等待就是一场灾难,因为根据抛出异常的位置,它可能会更加模糊。
至少当它在 try 块之外声明时,您可以确定抛出异常时该变量至少可能是什么;try 块之前的变量值。
当您声明局部变量时,它被放置在堆栈上(对于某些类型,对象的整个值将位于堆栈上,对于其他类型,只有引用将位于堆栈上)。当 try 块内出现异常时,该块内的局部变量将被释放,这意味着堆栈将“展开”回到 try 块开始时的状态。这是设计使然。这就是 try / catch 能够退出块内所有函数调用并将系统恢复到功能状态的方式。如果没有这种机制,当异常发生时,您将永远无法确定任何事物的状态。
让错误处理代码依赖于外部声明的变量,这些变量的值在 try 块内更改,对我来说似乎是糟糕的设计。您所做的本质上是故意泄漏资源以获取信息(在这种特殊情况下,这还不错,因为您只是泄漏信息,但想象一下如果它是其他资源?你只会让自己未来的生活变得更加艰难)。如果您需要更精细的错误处理,我建议将您的 try 块分解为更小的块。
当你有一个 try catch 时,你最多应该知道它可能抛出的错误。这些异常类通常会告诉您有关异常的所有信息。如果没有,您应该创建自己的异常类并传递该信息。这样,您将永远不需要从 try 块内部获取变量,因为异常是自我解释的。因此,如果您需要大量执行此操作,请考虑您的设计,并尝试考虑是否有其他方法,您可以预测即将到来的异常,或者使用来自异常的信息,然后可能重新抛出您自己的异常例外并提供更多信息。
正如其他用户所指出的,花括号定义了我所知道的几乎所有 C 风格语言的范围。
如果它是一个简单的变量,那么你为什么关心它在范围内的长度?这没什么大不了的。
在C#中,如果它是一个复杂的变量,你将需要实现IDisposable。然后,您可以使用 try/catch/finally 并在 finally 块中调用 obj.Dispose() 。或者您可以使用 using 关键字,它将自动调用代码部分末尾的 Dispose。
在 Python 中,如果声明它们的行没有抛出异常,它们在 catch/finally 块中可见。
如果在变量声明之上的某些代码中抛出异常怎么办?这意味着,在本例中声明本身并未发生。
try {
//doSomeWork // Exception is thrown in this line.
String s;
//doRestOfTheWork
} catch (Exception) {
//Use s;//Problem here
} finally {
//Use s;//Problem here
}
虽然在您的示例中它不起作用很奇怪,但采用类似的示例:
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);
}
}
我的想法是,因为 try 块中的某些内容触发了异常,所以它的名称空间内容不可信 - 即引用 catch 块中的 String 可能会导致抛出另一个异常。
好吧,如果它不抛出编译错误,并且您可以为该方法的其余部分声明它,那么就无法仅在 try 范围内声明它。它迫使您明确变量应该存在的位置并且不做出假设。
如果我们暂时忽略范围块问题,那么在没有明确定义的情况下,编译器将不得不更加努力地工作。虽然这并非不可能,但作用域错误也迫使您(代码的作者)意识到您编写的代码的含义(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 块内的变量状态提供更好的保证。它存在并且已被初始化。
请注意,语言设计者在其他结构方面做得很好,例如 使用 和 锁 问题和范围被明确定义,这使您可以编写更清晰的代码。
例如这 使用 关键字与 I一次性 对象位于:
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 难以理解,请尝试重构或引入另一层间接层,其中包含一个中间类,该中间类封装了您要完成的任务的语义。如果没有看到真正的代码,就很难更具体。
可以声明公共属性,而不是局部变量;这也应该避免未分配变量的另一个潜在错误。公共字符串 S { 得到;放;}
这 C# 规范 (15.2) 指出“在块中声明的局部变量或常量的范围就是该块”。
(在第一个示例中,try 块是声明“s”的块)
如果赋值操作失败,您的 catch 语句将返回一个指向未赋值变量的空引用。
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;
})();