为什么在“catch”或“finally”的范围内的“try”中没有声明variables?
在C#和Java(也可能是其他语言)中,在“try”块中声明的variables不在相应的“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中,编译错误是“无法parsing”;在C#中,“名称在当前上下文中不存在”)。
这个问题的一般解决scheme似乎是在try块之前而不是在try块内声明variables:
String s; try { s = "test"; // (more code...) } catch { Console.Out.WriteLine(s); //Java fans: think "System.out.println" here instead }
但是,至less对我来说,(1)这感觉像一个笨重的解决scheme,(2)它导致variables的范围比程序员想要的更大(整个方法的其余部分,而不是仅仅在上下文中的try-catch-最后)。
我的问题是,这个语言devise决定(Java,C#和/或任何其他适用的语言)背后的基本原理是什么?
两件事情:
-
一般来说,Java只有两个层次的范围:全局和function。 但是,try / catch是一个例外(不是双关语)。 当引发exception并且exception对象获得分配给它的variables时,该对象variables只在“catch”部分中可用,并在catch完成后立即销毁。
-
(更重要的是)。 您无法知道try块中的exception是在哪里引发的。 它可能是你的variables声明之前。 因此,不可能说catch / finally子句有哪些variables可用。 考虑以下情况,其范围如您所示:
try { throw new ArgumentException("some operation that throws an exception"); string s = "blah"; } catch (e as ArgumentException) { Console.Out.WriteLine(s); }
这显然是一个问题 – 当你到达exception处理程序时,s将不会被声明。 鉴于捕获是为了处理exception情况,最终必须执行,并且在编译时声明这个问题比运行时要好得多。
你怎么能确定,你已经到达了你的catch块的声明部分? 如果实例化抛出exception呢?
传统上,在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子句。
无论如何,在C ++中,自动variables的范围受到围绕它的大括号的限制。 为什么有人会认为这是不同的花式大括号之外的尝试关键字?
其他人已经提出了基本要求 – 在一个区块发生的事情停留在一个块。 但在.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和大多数inheritance了它的语法的语言是块范围的。 这意味着,如果一个variables被定义在一个块中,即在{}内部,那么它就是它的范围。
顺便说一下,例外的是JavaScript,它具有类似的语法,但是是函数范围的。 在JavaScript中,在try块中声明的variables在catch块的范围内,并且在其包含函数的其他地方。
像ravenspoint指出的那样,每个人都希望variables在它们被定义的块中是局部的。 try
引入一个块,然后catch
。
如果你想variables本地try
和catch
,尝试封闭两个块:
// here is some code { string s; try { throw new Exception(":(") } catch (Exception e) { Debug.WriteLine(s); } }
@burkhard有一个问题,为什么回答正确,但作为一个笔记我想补充,而你推荐的解决scheme的例子是99.9999 +%的时间好,这是不好的做法,使用之前检查为空在try块内实例化,或初始化variables,而不是在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块中做的事情比分配一个string更复杂,你也应该能够安全的从你的catch块访问数据。
根据MCTS Self-Paced Training Kit(考试70-536)第2课中的“如何抛出和抛出exception”部分:Microsoft®.NET Framework 2.0-应用程序开发基础 ,原因是可能发生了exception在try块中的variables声明之前(其他人已经注意到了)。
从页面25引用:
“注意,StreamReader声明被移到了前面例子中的Try块之外,这是必须的,因为Finally块不能访问在Try块中声明的variables, 这是有道理的,因为根据发生exception的地方,variables声明尝试块可能尚未执行 。“
正如大家已经指出的那样,答案就是“块如何定义”。
有一些build议使代码更漂亮。 请参阅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
你的解决scheme正是你应该做的。 你不能确定你的声明甚至在try块中到达,这将导致catch块中的另一个exception。
它只是作为单独的范围工作。
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
variables是块级别,并限制为Try或Catch块。 类似于在if语句中定义variables。 想想这个情况。
try { fileOpen("no real file Name"); String s = "GO TROJANS"; } catch (Exception) { print(s); }
string永远不会被声明,所以不能被依赖。
因为try块和catch块是2个不同的块。
在下面的代码中,你会期望在块A中定义的块在块B中是可见的吗?
{ // block A string s = "dude"; } { // block B Console.Out.WriteLine(s); // or printf or whatever }
在你给出的具体例子中,初始化s不能抛出exception。 所以你会认为它的范围可以扩展。
但一般来说,初始化expression式可能会抛出exception。 对于初始化程序抛出一个exception(或者在发生的另一个variables之后声明的)variables处于catch / finally范围的variables是没有意义的。
而且,代码的可读性会受到影响。 C中的规则(以及其后的语言,包括C ++,Java和C#)很简单:variables作用域跟在块之后。
如果你想让一个variables处于try / catch / finally的范围内,而不是其他地方,那么把所有的东西包装在另一个大括号(一个裸块)中,然后在try之前声明variables。
他们不在同一个范围内的部分原因是因为在try块的任何一点,你都可以抛出exception。 如果它们在同一个范围内,那么就是在等待一场灾难,因为取决于抛出exception的地方,这可能更加模糊。
至less当它在try块之外声明的时候,你肯定知道抛出exception时最小variables是什么。 try块之前的variables值。
While in your example it is weird that it does not work, take this similar one: try { //Code 1 String s = "1|2"; //Code 2 } catch { Console.WriteLine(s.Split('|')[1]); } This would cause the catch to throw a null reference exception if Code 1 broke. Now while the semantics of try/catch are pretty well understood, this would be an annoying corner case, since s is defined with an initial value, so it should in theory never be null, but under shared semantics, it would be. Again this could in theory be fixed by only allowing separated definitions (String s; s = "1|2";), or some other set of conditions, but it is generally easier to just say no. Additionally, it allows the semantics of scope to be defined globally without exception, specifically, locals last as long as the {} they are defined in, in all cases. Minor point, but a point. Finally, in order to do what you want, you can add a set of brackets around the try catch. Gives you the scope you want, although it does come at the cost of a little readability, but not too much. { String s; try { s = "test"; //More code } catch { Console.WriteLine(s); } }
当你声明一个局部variables时,它被放置在堆栈上(对于某些types,对象的整个值将在堆栈上,对于其他types,只有一个引用会在堆栈上)。 当try块内存在exception时,块内的局部variables被释放,这意味着堆栈被“解开”返回到try块开始时的状态。 这是devise。 这是try / catch如何退出块中的所有函数调用,并将系统恢复到function状态。 没有这个机制,当发生exception时,你永远无法确定任何事情的状态。
让你的error handling代码依赖于在try块中改变其值的外部声明的variables对我来说看起来是不好的devise。 你所做的事情本质上是为了获取信息而有意地泄漏资源(在这种情况下,它并不是那么糟糕,因为你只是泄露了信息,而是想象它是否是一些其他的资源?)你只是让自己的生活更加艰难未来)。 如果你需要更多的error handling粒度,我会build议把你的try块分成更小的块。
当你有一个尝试抓住,你应该大部分知道它可能抛出的错误。 Theese Exception类通常会告诉你所需要的关于exception的一切。 如果没有,你应该让你自己的exception类,并传递信息。 这样,你将永远不需要从try块内部获取variables,因为Exception是自我解释性的。 所以如果你需要做很多事情,那就想想你是在devise,然后试着想一下,如果有其他的方法,你可以预测exception来临,或者使用来自exception的信息,然后再重新抛出你自己的除了更多的信息。
正如其他用户所指出的那样,大括号在我所知的几乎所有C语言语言中都定义了范围。
如果它是一个简单的variables,那么你为什么要关心它将在多长时间? 这不是什么大事。
在C#中,如果它是一个复杂的variables,你会想实现IDisposable。 然后你可以使用try / catch / finally,并在finally块中调用obj.Dispose()。 或者您可以使用using关键字,它将自动调用代码部分末尾的Dispose。
在Python中,如果声明它们的行没有抛出,它们在catch / finally块中是可见的。
如果在variables声明之上的某些代码中引发exception呢? 这意味着,在这种情况下,声明本身并没有发生。
try { //doSomeWork // Exception is thrown in this line. String s; //doRestOfTheWork } catch (Exception) { //Use s;//Problem here } finally { //Use s;//Problem here }
我的想法是,因为try块中的某些东西触发了exception,所以它的名字空间内容不能被信任,即在catch块中引用String''可能会导致抛出另一个exception。
那么如果它不会抛出一个编译错误,并且可以为方法的其余部分声明它,那么就没有办法只在try范围内声明它。 这迫使你对variables应该存在的地方做出明确的规定,而不是做出假设。
如果赋值操作失败,你的catch语句将返回空指针给未赋值的variables。
C#Spec (15.2)规定“块中声明的局部variables或常量的范围在块中”。
(在你的第一个例子中,try块是声明了“s”的块)
问候,tamberg
如果我们暂时忽略范围限定问题,那么在没有明确定义的情况下,编译器将不得不更加努力。 虽然这不是不可能的,但范围错误也会迫使您,代码的作者意识到您所编写的代码的含义(即在catch块中strings可能为空)。 如果你的代码是合法的,在OutOfMemoryexception的情况下,甚至不能保证分配一个内存插槽:
// won't compile! try { VeryLargeArray v = new VeryLargeArray(TOO_BIG_CONSTANT); // throws OutOfMemoryException string s = "Help"; } catch { Console.WriteLine(s); // whoops! }
CLR(也就是编译器)也迫使你在使用它们之前初始化variables。 在提出的catch块中,不能保证这一点。
所以我们最终编译器不得不做很多工作,实际上并没有提供太多的好处,而且很可能会混淆人们,并引导他们问为什么try / catch有不同的作用。
除了一致性之外,通过不允许任何花哨和遵守已经在整个语言中使用的已经build立的范围语义,编译器和CLR能够更好地保证catch块中variables的状态。 它存在并已被初始化。
请注意,语言devise者已经在其他结构(如使用和locking问题和范围已经很好定义)方面做了很好的工作,这使得您可以编写更清晰的代码。
例如在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很难理解,可以尝试重构或引入另一层间接的中间类,它封装了你正在尝试完成的语义。 没有看到真正的代码,很难更具体。
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; })();
可以宣布公共财产,而不是局部variables; 这也应该避免未分配variables的另一个潜在错误。 公共stringS {get; 组; }