是否有理由使用 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;
}

只是风格上还是我缺少一些副作用?

是否有理由使用 Try/Final 和 ExceptionThrow 变量而不是 Try/Catch

这段代码有两个原因。 是的,格雷格提到,它不需要重新抛出异常。但这不是真正的原因。

真正的原因是语义问题。 不应使用异常来处理控制流。 这样做是处理这样一个事实,即如果在按钮内引发异常,它可以将按钮的视觉状态保留为"按下"。 这并不是真正"处理"异常。 这只是在引发异常时纠正视觉问题。

此代码不关心异常是什么,并且它不想捕获所有异常,因为这是不好的做法。 此外,代码没有做任何异常的事情。它只是说"嘿,如果我们到了函数的尽头,那么我们都很好。 如果我们没有,那么让我们重置按钮状态以确保"。

因此,这不是真正的异常处理,因此它不会捕获异常。 它只是注意到抛出了一个异常并进行了一些清理。

编辑:

此方法的争议可能较小,如果简单地像这样重命名,删除对异常的任何引用,则更有意义:

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是一个虚拟方法。它应该被用户编写的代码覆盖,这可能会引发任意异常。所写的代码表达了"从那些推翻这种方法的人造成的混乱中清理"的语义。