【问题标题】:Is there a reason for using Try/Finally with ExceptionThrown variable over Try/Catch是否有理由在 Try/Catch 上使用带有 ExceptionThrown 变量的 Try/Finally
【发布时间】:2013-09-14 04:41:53
【问题描述】:

我正在阅读 .Net Reference Source 并在第 408 行的 ButtonBase.cs 中找到了这个宝石:

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{} catch {} 比 throw 更糟糕,而且您只是在压碎堆栈。
  • 我认为两者没有区别。然而,如果你写这样的东西,因为代码会随着时间的推移而改变,总有一天你需要来自异常的信息,你最终会使用 catch。不过变化很小。

标签: c# coding-style


【解决方案1】:

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

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

这段代码不关心异常是什么,也不想捕获所有异常,因为这是不好的做法。此外,代码没有做任何异常......它只是说“嘿,如果我们到达函数的末尾,那么我们都很好。如果我们没有,那么让我们重置按钮状态只是为了确定”。

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

编辑:

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

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; 重新抛出异常时,它不会简单地传递现有异常,它必须重新创建异常并重新抛出它,花费的时间是原来的两倍。

正如我们所见,捕获异常和不捕获之间没有显着区别,而是使用 finally。但是,重新抛出异常是成本的来源。

这是在 VS 2012 Update 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

【讨论】:

  • 无论哪种方式,它都是一段不错的、聪明的代码,它避免了 catch 子句的 stackwalk 成本。但是我对将bool 重命名为cleanupRequired 有点不确定,因为它完全隐藏了我们知道这一点的机制。在某些时候,你可能会变得有点聪明,接下来你知道,有人在 StackOverflow 上问每个人,“WTF 他们在做 this 时是否在想?” ;-)
  • 这似乎更像是一种风格上的区别,而不是功能上的区别。实际上,您似乎只是因为名称而重新实现了 catch 的语义。其他互操作或易发生异常的代码当然不使用标志来实现清理。
  • @Mitch - 我认为这根本不是风格。 OnClick() 可能会执行用户编写的代码,因此它几乎可以做任何事情。虽然包罗万象可以工作,但没有正当理由会产生额外的开销。这段代码只是注意到 OnClick 调用没有正常返回,并进行了一些清理。它不在乎什么或如何,所以为什么要抓住你并不真正关心的东西呢?
  • @MystereMan,您似乎暗示有一些要求要求catch 语句处理异常,当重新抛出它们的能力允许它们在这种情况下使用时。此外,问题在于是否存在功能差异,而不是风格或语义差异。
  • 另外,捕获带来的“开销”是什么?它是否比 3 个额外的 LOC 和一个令人困惑的标志引入的可能错误更多开销?除此之外,我确实关心异常 - 也许不是它的类型,但我关心它是否发生。依赖一行代码的不执行似乎在语义上更加异常。 finally 块的语义是它“总是执行,无论是否发生异常”,而 catch 是“用于捕获 [try] 块执行期间发生的异常”
【解决方案2】:

如果你使用 try,catch 这样的异常必须被抛出两次。使用try,最后只抛出一个异常,所以它很多效率更高,尤其是如果这段代码经常被调用。

【讨论】:

  • 如果异常代码被经常调用,那么它就不再是异常了。对错误代码路径进行性能优化通常投资回报率很低。
  • +1 不错的答案,是第一个支持的人之一......但我已经看到了我的优化分享,我必须告诉你,这不是真的 - 代码不是“效率更高”实际上只提高了大约 10-15% 的效率,这意味着存在优化并且实际上没有发生 2 次堆栈遍历,因为在第一个示例中再次抛出异常。 Here is a test app 看起来使用 finally 技巧稍微快一点,实际上太小以至于不值得用 finally 子句编写混淆代码。
  • 所以我们所说的可能是在try/catch 示例上执行了 2-3 个额外的指令,考虑到实际示例中有一个实际的方法调用,这将是不引人注意的优化并且可以是被忽略为性能改进...或多或少,但对于不错的收获仍然+1
  • 这不仅仅是关于性能。声称处理异常,但实际上并未处理它们,这也使调试变得复杂,因为调试器无法提前知道异常何时抛出,是否会被处理。如果您的自定义 Click 处理程序引发了您不知道的异常,并且您正在调试您的程序,那么您通常希望确切地知道发生了什么。避免双重 throw 在这里有帮助。
  • 我几乎总是将调试器设置为在抛出异常时停止。这样,如果几乎每次运行都会抛出异常,我会尝试通过使用非抛出替代方案来排除它。该框架曾经有很多 API,如果不将 try...catch 块包裹在其周围(例如,Parse() 系列方法),您将无法测试非异常异常,但多年来情况有了很大改善(例如,TryParse())。
【解决方案3】:

显然,throw; 有副作用,它仍然会通过至少 .Net 2 删除堆栈跟踪信息。我想这个语法被用来解决早期版本的框架上的 throw; 的实现.

This blog article 给出了两个例子,说明throw; 不等于从不捕获异常。

Showing stack trace of exception that is re-thrown, rather than stack trace from throw point 的问题给出了一个场景,即重新抛出异常会导致 Visual Studio 中的不同操作。

【讨论】:

  • 我应该认为这是真正的答案。避免重新抛出,因此有准确的堆栈跟踪。
【解决方案4】:

嗯,我能想到的一个原因是(将来)增加了捕获特定异常的需求,同时保持相同的清理行为。考虑以下示例:

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();
    } 
}

【讨论】:

    【解决方案5】:

    一个小提示:OnClick 是一个虚方法。它预计会被用户编写的代码覆盖,这可能会引发任意异常。编写的代码表达了“清理那些覆盖此方法的人所造成的混乱”的语义。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-06-01
      • 1970-01-01
      • 1970-01-01
      • 2014-11-27
      • 1970-01-01
      • 2012-03-06
      • 2018-10-24
      • 1970-01-01
      相关资源
      最近更新 更多