为啥“catch”或“finally”范围内的“try”中没有声明变量?
Posted
技术标签:
【中文标题】为啥“catch”或“finally”范围内的“try”中没有声明变量?【英文标题】:Why aren't variables declared in "try" in scope in "catch" or "finally"?为什么“catch”或“finally”范围内的“try”中没有声明变量? 【发布时间】:2010-09-10 19:59:51 【问题描述】:在 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 cannot be resolved”;在C#中,是“The name 's' does not exist 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# 和/或任何其他适用的语言中)?
【问题讨论】:
【参考方案1】:两件事:
通常,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 旨在处理异常情况,并且 finally 必须执行,因此在编译时安全并声明这是一个问题要比在运行时好得多。
【讨论】:
【参考方案2】:您如何确定您到达了 catch 块中的声明部分?如果实例化抛出异常怎么办?
【讨论】:
【参考方案3】:传统上,在 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 子句。
【讨论】:
【参考方案4】:其他人都提出了基础知识——块中发生的事情留在块中。但在 .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 规范中也是如此。
范围可以保护您,但您有时必须解决它。随着时间的推移,你会习惯它,它开始感觉自然。就像其他人所说的那样,一个区块中发生的事情会留在那个区块中。你想分享一些东西吗?你必须走出街区......
【讨论】:
【参考方案5】:无论如何,在 C++ 中,自动变量的范围受到围绕它的花括号的限制。为什么有人会期望通过在花括号外插入 try 关键字来实现这一点?
【讨论】:
同意; “”表示范围结束。然而,try-catch-finally 是不寻常的,因为在一个 try 块之后,您 必须 有一个 catch 和/或 finally 块;因此,正常规则的例外情况是,将 try 块的范围带入关联的 catch/finally 似乎可以接受?【参考方案6】:就像 ravenspoint 指出的那样,每个人都希望变量在定义它们的块中是本地的。try
引入了一个块,catch
也是如此。
如果您想要 try
和 catch
的本地变量,请尝试将两者都包含在一个块中:
// here is some code
string s;
try
throw new Exception(":(")
catch (Exception e)
Debug.WriteLine(s);
【讨论】:
【参考方案7】:简单的答案是 C 和大多数继承其语法的语言都是块作用域的。这意味着如果一个变量被定义在一个块中,即在 内,那就是它的作用域。
顺便说一下,javascript 是个例外,它有类似的语法,但它是函数作用域的。在 JavaScript 中,在 try 块中声明的变量在 catch 块的范围内,在其包含函数的任何其他地方。
【讨论】:
【参考方案8】:根据 MCTS 自定进度培训套件(考试 70-536)第 2 课中标题为“如何抛出和捕获异常”的部分:Microsoft® .NET Framework 2.0 - 应用程序开发基础 ,原因是异常可能发生在 try 块中的变量声明之前(正如其他人已经指出的那样)。
引自第 25 页:
“请注意,在前面的示例中,StreamReader 声明已移到 Try 块之外。这是必要的,因为 finally 块无法访问在 Try 块中声明的变量。这是有道理的,因为取决于发生异常时,Try 块中的变量声明可能尚未执行。"
【讨论】:
【参考方案9】:@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 块中的数据。
【讨论】:
【参考方案10】:正如每个人都指出的那样,答案几乎是“这就是定义块的方式”。
有一些建议可以使代码更漂亮。见ARM
try (FileReader in = makeReader(), FileWriter out = makeWriter())
// code using in and out
catch(IOException e)
// ...
Closures 也应该解决这个问题。
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
【讨论】:
【参考方案11】:您的解决方案正是您应该做的。您甚至无法确定您的声明是否已在 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
【讨论】:
【参考方案12】:变量是块级别的,并且仅限于该 Try 或 Catch 块。类似于在 if 语句中定义变量。想想这种情况。
try
fileOpen("no real file Name");
String s = "GO ***S";
catch (Exception)
print(s);
字符串永远不会被声明,所以它不能被依赖。
【讨论】:
【参考方案13】:因为 try 块和 catch 块是 2 个不同的块。
在下面的代码中,您是否希望块 A 中定义的 s 在块 B 中可见?
// block A
string s = "dude";
// block B
Console.Out.WriteLine(s); // or printf or whatever
【讨论】:
【参考方案14】:在 Python 中,如果声明它们的行没有抛出,它们在 catch/finally 块中可见。
【讨论】:
【参考方案15】:虽然在您的示例中它不起作用很奇怪,但请使用类似的示例:
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);
【讨论】:
【参考方案16】:在您给出的具体示例中,初始化 s 不能引发异常。所以你会认为它的范围可能会扩大。
但一般来说,初始化表达式可以抛出异常。对于初始化器引发异常(或在发生异常的另一个变量之后声明的)的变量在 catch/finally 的范围内是没有意义的。
此外,代码可读性也会受到影响。 C(以及遵循它的语言,包括 C++、Java 和 C#)的规则很简单:变量作用域遵循块。
如果您希望变量在 try/catch/finally 的范围内,但不在其他范围内,则将整个内容包裹在另一组大括号(裸块)中,并在 try 之前声明变量。
【讨论】:
【参考方案17】:它们不在同一范围内的部分原因是因为在 try 块的任何时候,您都可以抛出异常。如果它们在同一个范围内,等待是一场灾难,因为根据抛出异常的位置,它可能会更加模棱两可。
至少当它在 try 块之外声明时,您至少可以确定在抛出异常时该变量可能是什么; try 块之前的变量值。
【讨论】:
【参考方案18】:当你声明一个局部变量时,它被放置在堆栈上(对于某些类型,对象的整个值将在堆栈上,对于其他类型,只有一个引用将在堆栈上)。当 try 块内发生异常时,块内的局部变量被释放,这意味着堆栈被“展开”回它在 try 块开始时的状态。这是设计使然。这就是 try / catch 能够退出块内的所有函数调用并将您的系统恢复到功能状态的方式。如果没有这种机制,您将永远无法确定异常发生时的任何状态。
让你的错误处理代码依赖于外部声明的变量,这些变量的值在 try 块内发生了变化,这对我来说似乎是糟糕的设计。您所做的实际上是为了获取信息而故意泄漏资源(在这种特殊情况下,这并没有那么糟糕,因为您只是在泄漏信息,但是想象一下它是否是其他资源?您只是在使自己的生活更加艰难未来)。如果您需要更精细的错误处理,我建议您将 try 块分解成更小的块。
【讨论】:
【参考方案19】:当你有一个 try catch 时,你应该最多知道它可能抛出的错误。 Theese Exception 类通常会告诉您有关异常的所有信息。如果没有,您应该创建自己的异常类并传递该信息。这样,您将永远不需要从 try 块中获取变量,因为异常是自我解释的。因此,如果您需要做很多事情,请考虑一下您的设计,并尝试考虑是否有其他方式,您可以预测异常的到来,或者使用来自异常的信息,然后重新抛出您自己的有更多信息的例外。
【讨论】:
【参考方案20】:正如其他用户所指出的,花括号定义了我所知道的几乎所有 C 风格语言中的范围。
如果它是一个简单的变量,那么你为什么要关心它在范围内的时间?这没什么大不了的。
在 C# 中,如果它是一个复杂的变量,您将需要实现 IDisposable。然后,您可以使用 try/catch/finally 并在 finally 块中调用 obj.Dispose()。或者你可以使用 using 关键字,它会在代码段末尾自动调用 Dispose。
【讨论】:
【参考方案21】:如果在变量声明之上的某些代码中抛出异常怎么办。这意味着,在这种情况下,声明本身并没有发生。
try
//doSomeWork // Exception is thrown in this line.
String s;
//doRestOfTheWork
catch (Exception)
//Use s;//Problem here
finally
//Use s;//Problem here
【讨论】:
【参考方案22】:C# Spec (15.2) 声明“在块中声明的局部变量或常量的范围就是块。”
(在您的第一个示例中,try 块是声明“s”的块)
【讨论】:
【参考方案23】:我的想法是,因为 try 块中的某些东西触发了异常,所以它的命名空间内容不能被信任 - 即在 catch 块中引用 String 's' 可能会导致另一个异常的抛出。
【讨论】:
【参考方案24】:好吧,如果它没有抛出编译错误,并且您可以为方法的其余部分声明它,那么就没有办法只在 try 范围内声明它。它迫使您明确说明变量应该存在的位置并且不做出假设。
【讨论】:
【参考方案25】:如果我们暂时忽略范围块问题,编译器将不得不在没有明确定义的情况下更加努力地工作。虽然这并非不可能,但范围错误也会迫使您(代码的作者)意识到您编写的代码的含义(字符串 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 块内的变量状态提供更大的保证。它存在并已被初始化。
请注意,语言设计者在问题和范围明确定义的其他结构(如 using 和 lock 方面做得很好,这使您可以编写更清晰的代码.
例如using 关键字与 IDisposable 对象在:
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 难以理解,请尝试重构或引入另一层间接层,其中包含封装您尝试完成的语义的中间类。没有看到真实的代码,很难更具体。
【讨论】:
【参考方案26】:可以声明公共属性,而不是局部变量;这也应该避免未分配变量的另一个潜在错误。 公共字符串 S 获取;放;
【讨论】:
【参考方案27】:如果赋值操作失败,您的 catch 语句将对未赋值变量有一个空引用。
【讨论】:
它未分配。它甚至不是 null(与实例和静态变量不同)。【参考方案28】: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;
)();
【讨论】:
WTF?为什么投反对票?封装对于 OOP 是不可或缺的。看起来也很漂亮。 我没有投反对票,但问题是返回一个未初始化的字符串。以上是关于为啥“catch”或“finally”范围内的“try”中没有声明变量?的主要内容,如果未能解决你的问题,请参考以下文章
为啥 try..catch..finally 块的 finally 节在 catch 之前运行?
求教大神,java中的jdbc程序为啥要加finally,不是加了try catch以后,后面的语句就会执行了啊