是否有理由使用 Try/Final 和 ExceptionThrow 变量而不是 Try/Catch
本文关键字:Try 变量 Catch ExceptionThrow Final 有理由 是否 | 更新日期: 2023-09-27 18:31:20
我正在仔细阅读.Net引用源,并在ButtonBase中找到了这个宝石.cs在第408行:
bool exceptionThrown = true;
try
{
OnClick();
exceptionThrown = false;
}
finally
{
if (exceptionThrown)
{
// Cleanup the buttonbase state
SetIsPressed(false);
ReleaseMouseCapture();
}
}
问题是,什么会促使某人使用 exceptionThrown
标志而不是仅仅将其写成
try
{
OnClick();
}
catch
{
SetIsPressed(false);
ReleaseMouseCapture();
throw;
}
只是风格上还是我缺少一些副作用?
这段代码有两个原因。 是的,格雷格提到,它不需要重新抛出异常。但这不是真正的原因。
真正的原因是语义问题。 不应使用异常来处理控制流。 这样做是处理这样一个事实,即如果在按钮内引发异常,它可以将按钮的视觉状态保留为"按下"。 这并不是真正"处理"异常。 这只是在引发异常时纠正视觉问题。
此代码不关心异常是什么,并且它不想捕获所有异常,因为这是不好的做法。 此外,代码没有做任何异常的事情。它只是说"嘿,如果我们到了函数的尽头,那么我们都很好。 如果我们没有,那么让我们重置按钮状态以确保"。
因此,这不是真正的异常处理,因此它不会捕获异常。 它只是注意到抛出了一个异常并进行了一些清理。
编辑:
此方法的争议可能较小,如果简单地像这样重命名,删除对异常的任何引用,则更有意义:
bool cleanupRequired = true;
try
{
OnClick();
cleanupRequired = false;
}
finally
{
if (cleanupRequired)
{
// Cleanup the buttonbase state
SetIsPressed(false);
ReleaseMouseCapture();
}
}
编辑:
为了支持我在下面的评论,我编写了以下测试程序来测试场景:
static void Main(string[] args)
{
TimeSpan ts = new TimeSpan();
TimeSpan ts2 = new TimeSpan();
TimeSpan ts3 = new TimeSpan();
TimeSpan ts4 = new TimeSpan();
TimeSpan ts5 = new TimeSpan();
TimeSpan ts6 = new TimeSpan();
TimeSpan ts7 = new TimeSpan();
TimeSpan ts8 = new TimeSpan();
Stopwatch sw = new Stopwatch();
// throw away first run
for (int i = 0; i < 2; i++)
{
sw.Restart();
try
{
throw new NotImplementedException();
}
catch
{
ts = sw.Elapsed;
}
sw.Stop();
ts2 = sw.Elapsed;
try
{
sw.Restart();
try
{
throw new NotImplementedException();
}
finally
{
ts3 = sw.Elapsed;
}
}
catch
{
ts4 = sw.Elapsed;
}
sw.Stop();
ts5 = sw.Elapsed;
try
{
sw.Restart();
try
{
throw new NotImplementedException();
}
catch
{
ts6 = sw.Elapsed;
throw;
}
}
catch
{
ts7 = sw.Elapsed;
}
sw.Stop();
ts8 = sw.Elapsed;
}
Console.WriteLine(ts);
Console.WriteLine(ts2);
Console.WriteLine(ts3);
Console.WriteLine(ts4);
Console.WriteLine(ts5);
Console.WriteLine(ts6);
Console.WriteLine(ts7);
Console.WriteLine(ts8);
Console.ReadLine();
}
我得到了以下结果(我将它们分开以使它们更易于阅读):
00:00:00.0028424
00:00:00.0028453
00:00:00.0028354
00:00:00.0028401
00:00:00.0028427
00:00:00.0028404
00:00:00.0057907
00:00:00.0057951
最后 3 个表明,当使用 throw;
重新抛出异常时,它不会简单地传递现有异常,它必须重新创建异常并重新抛出它,花费两倍的时间。
正如我们所看到的,捕获异常和不捕获之间没有显着区别,而是使用 final。 但是,重新引发异常是成本所在。
这在VS 2012更新3中运行。
编辑:
没有调试器的计时。 如您所见,重新投掷的成本仍然是其两倍:
00:00:00.0000149
00:00:00.0000154
00:00:00.0000137
00:00:00.0000140
00:00:00.0000146
00:00:00.0000137
00:00:00.0000248
00:00:00.0000251
如果使用 try,则捕获这样的异常必须抛出两次。使用 try,最后只抛出一个异常,因此效率要高得多,尤其是在经常调用此代码的情况下。
显然,throw;
存在副作用,至少在 .Net 2 之前仍会删除堆栈跟踪信息。 我想这种语法用于解决框架早期版本上throw;
的实现。
这篇博客文章给出了两个例子,说明throw;
不等于根本不捕获异常。
而不是从引发点开始的堆栈跟踪给出了一种方案,即重新引发异常会导致 Visual Studio 中的不同操作。
好吧,我能想到的一个原因是(将来)添加捕获特定异常的需求,同时保持相同的清理行为。请考虑以下示例:
try
{
OnClick();
}
catch(System.SomeSpecificException ex)
{
Handle(ex);
throw;
// now, we're missing the cleanup
}
catch
{
SetIsPressed(false);
ReleaseMouseCapture();
throw;
}
与。
var exceptionThrown = true;
try
{
OnClick();
exceptionThrown = false;
}
catch(System.SomeSpecificException ex)
{
Handle(ex);
throw;
// whoa, I added specific handler, but cleanup is called anyway!
}
finally
{
if (exceptionThrown)
{
// Cleanup the buttonbase state
SetIsPressed(false);
ReleaseMouseCapture();
}
}
一个小说明:OnClick
是一个虚拟方法。它应该被用户编写的代码覆盖,这可能会引发任意异常。所写的代码表达了"从那些推翻这种方法的人造成的混乱中清理"的语义。