除了保持一致性之外,对于我们而言,无需重构就能够使用错误处理包装代码是否有意义?
要回答这个问题,不仅要考虑变量的作用域,还需要研究。
即使变量仍在范围内,也不会明确赋值。
在try块中声明变量可以向编译器和人类读者表达,它仅在该块内部有意义。对于编译器强制执行该操作很有用。
如果希望变量在try块之后,则可以在块外部声明它:
var zerothVariable = 1_000_000_000_000L;
int firstVariable;
try {
// Change checked to unchecked to allow the overflow without throwing.
firstVariable = checked((int)zerothVariable);
}
catch (OverflowException e) {
Console.Error.WriteLine(e.Message);
Environment.Exit(1);
}
这表示变量在try块之外可能是有意义的。编译器将允许这样做。
但是,这也表明了另一个原因,即在将try块中引入变量之后,将变量保持在范围内通常没有用。C#编译器执行定值分配分析,并禁止读取尚未证明已赋予其值的变量的值。因此,您仍然无法读取变量。
假设我尝试在try块之后读取变量:
Console.WriteLine(firstVariable);
这将产生一个编译时错误:
CS0165使用未分配的局部变量“ firstVariable”
我在catch块中调用了Environment.Exit,所以我知道在调用Console.WriteLine之前已分配了变量。但是编译器不会推断出这一点。
为什么编译器这么严格?
我什至不能这样做:
int n;
try {
n = 10; // I know this won't throw an IOException.
}
catch (IOException) {
}
Console.WriteLine(n);
解决此限制的一种方法是说C#中的明确赋值分析不是很复杂。但是另一种看待它的方式是,当您在带有catch子句的try块中编写代码时,您正在告诉编译器和任何人类读者都应该将其视为可能无法全部运行。
为了说明我的意思,请假设编译器是否允许上面的代码,但是您在try块中添加了一个对您自己知道不会引发异常的函数的调用。无法保证所调用的函数不会抛出IOException
,编译器无法知道n
已分配该函数,然后必须进行重构。
就是说,通过前面的高度复杂的分析来确定在try块中使用catch子句分配的变量之后是否已明确分配,编译器可帮助您避免编写可能在以后中断的代码。(毕竟,捕获异常通常意味着您认为可能会抛出异常。)
您可以确保通过所有代码路径分配变量。
您可以通过在try块之前或catch块中为变量提供一个值来使代码编译。这样,即使try块中的分配没有发生,它仍将被初始化或分配。例如:
var n = 0; // But is this meaningful, or just covering a bug?
try {
n = 10;
}
catch (IOException) {
}
Console.WriteLine(n);
要么:
int n;
try {
n = 10;
}
catch (IOException) {
n = 0; // But is this meaningful, or just covering a bug?
}
Console.WriteLine(n);
那些编译。但它是最好的,如果默认值,你给它有意义的只有做这样的事情*,并产生正确的行为。
请注意,在第二种情况下,您在try块和所有catch块中分配了变量,尽管您可以在try-catch之后读取变量,但是仍然无法读取附加finally
块内的变量,因为在比我们经常想的更多的情况下,执行可能会留下尝试障碍。
* 顺便说一下,某些语言(例如C和C ++)都允许未初始化的变量,并且没有明确的赋值分析以防止从中读取数据。由于读取未初始化的内存会导致程序以不确定和不稳定的方式运行,因此通常建议避免在不提供初始化程序的情况下以这些语言引入变量。在具有明确赋值分析的语言(例如C#和Java)中,编译器使您免于读取未初始化的变量,也避免了使用无意义的值对其进行初始化(其后会被误解为有意义)的较小危害。
您可以使未分配变量的代码路径引发异常(或返回)。
如果您打算执行某些操作(例如记录日志)并重新抛出该异常或引发另一个异常,并且这种情况发生在未分配变量的任何catch子句中,则编译器将知道该变量已被分配:
int n;
try {
n = 10;
}
catch (IOException e) {
Console.Error.WriteLine(e.Message);
throw;
}
Console.WriteLine(n);
编译后,很可能是一个合理的选择。但是,在实际的应用程序中,除非仅在甚至无法尝试恢复*的情况下抛出该异常,否则应确保仍在某个地方捕获并正确处理了该异常。
(在这种情况下,您也无法读取finally块中的变量,但是感觉不应该,毕竟,finally块基本上总是运行,在这种情况下,变量并非总是被分配)
* 例如,许多应用程序没有catch子句来处理一个OutOfMemoryException异常,因为任何他们能做些什么可能是至少一样糟糕崩溃。
也许你真的不想要重构代码。
在你的榜样,你介绍firstVariable
和secondVariable
在try块。就像我说过的那样,您可以在分配了它们的try块之前定义它们,以便它们随后仍在作用域内,并且您可以通过确保始终分配它们来满足/欺骗编译器以允许您读取它们。
但是这些块之后出现的代码大概取决于它们是否已正确分配。如果是这种情况,那么您的代码应反映并确保这一点。
首先,您可以(并且应该)实际处理那里的错误吗? 存在异常处理的原因之一是,即使在错误发生的地方附近,也可以更轻松地在可以有效处理的地方处理错误。
如果您实际上无法处理初始化并使用这些变量的函数中的错误,则也许try块根本不应该位于该函数中,而应该位于某个更高的位置(即,在调用该函数的代码中,或在代码中调用该代码)。只要确保您没有意外捕获到其他地方抛出的异常,并错误地假定在初始化firstVariable
and 时抛出了该异常secondVariable
。
另一种方法是将使用变量的代码放在try块中。这通常是合理的。同样,如果您从其初始值设定项捕获的异常也可能从周围的代码中抛出,则应确保在处理它们时不要忽略这种可能性。
(我假设您要使用比示例中所示的表达式更复杂的表达式来初始化变量,以使它们实际上可能引发异常,并且您实际上并没有计划捕获所有可能的异常,而是仅捕获任何特定的异常您可以预期并有意义地进行处理。确实,现实世界并不总是那么好,生产代码有时也可以做到这一点,但是由于您的目标是处理初始化两个特定变量时发生的错误,因此您为该特定变量编写的任何catch子句目的应特定于那些错误。)
第三种方法是将可能失败的代码以及处理失败的try-catch 提取到其自己的方法中。如果您可能想先完全处理错误,然后又不必担心无意中捕获了应该在其他地方处理的异常,则这很有用。
例如,假设您要在未能分配任何变量时立即退出应用程序。(显然,并非所有异常处理都是针对致命错误的;这仅是示例,并且可能不是您希望应用程序对问题做出反应的方式。)您可以这样做:
// In real life, this should be named more descriptively.
private static (int firstValue, int secondValue) GetFirstAndSecondValues()
{
try {
// This code is contrived. The idea here is that obtaining the values
// could actually fail, and throw a SomeSpecificException.
var firstVariable = 1;
var secondVariable = firstVariable;
return (firstVariable, secondVariable);
}
catch (SomeSpecificException e) {
Console.Error.WriteLine(e.Message);
Environment.Exit(1);
throw new InvalidOperationException(); // unreachable
}
}
// ...and of course so should this.
internal static void MethodThatUsesTheValues()
{
var (firstVariable, secondVariable) = GetFirstAndSecondValues();
// Code that does something with them...
}
该代码返回并使用C#7.0的语法解构一个ValueTuple以返回多个值,但是,如果您仍在使用C#的早期版本,则仍可以使用此技术。例如,您可以使用out参数,或者返回提供两个值的自定义对象。此外,如果这两个变量实际上并不紧密相关,那么最好还是使用两个单独的方法。
特别是如果您有多种方法,则应该考虑集中代码以通知用户致命错误并退出。(例如,您可以编写Die
带有message
参数的方法。)该throw new InvalidOperationException();
行从未真正执行过,因此您不必(也不应)为其编写catch子句。
除了在发生特定错误时退出之外,如果抛出包装原始异常的其他类型的异常,有时您可能还会编写类似以下的代码。(在这种情况下,你会不会需要一秒钟,可达throw表达式。)
结论:范围只是图片的一部分。
仅通过将变量的声明与其赋值分开,就可以实现不进行重构(或者,如果愿意,几乎不进行重构)的带有错误处理的代码包装效果。如果满足C#的明确赋值规则,则编译器允许这样做,并且在try块之前声明一个变量将使其更大的范围变得清晰。但是进一步重构可能仍然是您的最佳选择。
try.. catch
是一种特定类型的代码块,就所有代码块而言,就范围而言,您不能在一个变量中声明一个变量,而在另一个变量中使用同一变量。