TryFoo应该抛出异常吗?

时间:2009-07-22 17:24:51

标签: .net exception

.NET Framework中的一个常见模式是TryXXX模式(我不知道这是否就是他们真正称之为的模式),其中被调用方法尝试执行某些操作,如果成功则返回True ,如果操作失败,则为False。一个很好的例子是通用Dictionary.TryGetValue方法。

这些方法的文档说它们不会抛出异常:该失败将在方法返回值中报告。到目前为止一切都很好。

我最近遇到了两种不同的情况,其中实现TryXXX模式的.NET Framework方法引发了异常。有关详细信息,请参阅Bug in System.Random constructor?Uri.TryCreate throws UriFormatException?

在这两种情况下,TryXXX方法都会调用其他方法来引发意外异常,并且这些异常会被转义。我的问题:这是否打破了不抛出异常的隐含合同?

换句话说,如果您正在撰写TryFoo,您是否可以通过这样写来保证异常无法逃脱?

public bool TryFoo(string param, out FooThing foo)
{
    try
    {
        // do whatever
        return true;
    }
    catch
    {
        foo = null;
        return false;
    }
}

这很诱人,因为这可以保证不会逃避任何例外,从而兑现隐含的合同。但这是一个隐藏的bug。

根据我的经验,我的直觉是,这是一个非常糟糕的主意。但是如果TryFoo让一些异常逃脱,那么它真正说的是“我不会抛出任何我知道如何处理的异常”,然后是合同的整个想法“我不会抛出异常“被扔出窗外。

那么,你有什么看法? TryFoo应该处理所有异常,还是只处理它预期发生的异常?你的理由是什么?

12 个答案:

答案 0 :(得分:5)

这取决于。 TryParseFoo应该捕获所有解析异常,例如null或格式错误的字符串。但也有不相关的例外情况(例如ThreadAbortException),不应该自动安静。

在Python中(跟我一起),你可以通过捕获KeyboardInterrupt异常来解决很多问题,这基本上意味着你的程序即使你用Ctrl-C也能继续运行。

我的经验法则是,如果您想要TryFoo而不清理输入,则应使用Foo。如果TryFoo发现与Foo输入无关的错误,则会过于激进。

答案 1 :(得分:5)

不保证不抛出异常。在Dictionary上考虑这个简单的例子......

var dic = new Dictionary<string, string>() { { "Foo", "Bar" } };

string val = String.Empty;
string key = null;

dic.TryGetValue(key, out val); // oops

我发送了一个空键,我得到一个NullArgumentException。反射器因此显示代码...

private int FindEntry(TKey key)
{
  if (key == null)
  {
    ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
   }
   // more stuff
}

在我看来,合同的意思是“如果你试图做的事情失败了,我就不会抛出异常”,但这远非“我不会抛出任何例外”。既然你可能没有编写框架,你可能会妥协这样......

catch( Exception ex )
{
  Logger.Log( ex );
  Debug.Assert( false );
  foo = null;
  return false;
}  

...并且不处理具有该catch块的TryXXX失败案例(因此您没有一堆简单的日志条目)。在这种情况下,您至少会揭示错误的性质并在开发期间识别它。时间以及在运行时记下它。

答案 2 :(得分:3)

我认为TryFoo 不得抛出异常。我认为TryFoo的合同是它将处理无效输入作为正常情况,而不是例外情况。也就是说,如果由于输入无效而导致任务失败,则不会抛出异常;如果由于任何其他原因失败,它应该抛出异常。我在TryFoo返回后的期望是它处理了我的输入,或者我的输入无效。如果这些都不是真的,我想要一个例外,所以我知道它。

答案 3 :(得分:2)

您链接的另外两个StackOverflow问题似乎是真正的错误。例如Uri.TryCreate抛出UriFormatException的那个肯定是合同的中断,因为该方法的MSDN文档明确指出:

  

如果无法创建Uri,则不会抛出异常。

另一个,TryDequeue,我找不到任何文档,但它似乎也是一个bug。如果这些方法被称为 TryXXX ,但是文档明确声明它们可能会抛出异常,那么您可能会有一个观点。

通常会有这种方法的两个版本,一个抛出异常,另一个抛出异常。例如,Int32.ParseInt32.TryParse TryXXX 方法的要点不是隐藏异常。这适用于失败实际上并非特殊情况的情况。例如,如果您正在从控制台读取数字并希望继续尝试直到输入正确的数字,则您不希望继续捕获异常。同样,如果你想从字典中获取一个值,但它可能不存在是完全正常的情况,你可以使用TryGetValue

摘要 TryXXX 不应抛出异常,也不会隐藏异常。它适用于失败的正常和非异常情况,并且您希望轻松地检测到这种情况,而无需捕获异常的开销和精力。通常情况下,提供异常的非TryXXX方法也适用于例外情况。

答案 4 :(得分:1)

TryXXX背后的想法是它不会抛出通常在您调用相应的XXX调用时通常会发生的特定类型的异常 - 该方法的使用者知道他们忽略任何通常会发生的异常。

  

这很诱人,因为这样可以保证不会逃避任何例外,从而兑现隐含的合同。

这不完全正确 - 可以逃避try/catch(例如OutOfMemoryExceptionStackOverflowException等)的一些例外情况。

答案 5 :(得分:1)

你几乎不应该抓住所有例外。如果您必须使用围绕XXX的TryXXX对(而不是首选,XXX根据TryXXX实施并投掷错误结果)实施try/catch,请捕获特定的预期异常(s)only。

如果存在语义上不同的问题,TryXXX可以抛出,例如程序员错误(可能在不期望的地方传递null)。

答案 6 :(得分:1)

这取决于方法尝试做什么。

如果方法例如从数据读取器读取字符串值并尝试将其解析为整数,则在解析失败时不应抛出异常,但如果数据读取器无法读取,则应抛出异常数据库或者您正在阅读的值根本不是字符串。

该方法应遵循捕获异常的一般原则,您应该只捕获您知道如何处理的异常。如果发生了一些完全不同的异常,你应该让它泡到其他知道如何处理它的代码。

答案 7 :(得分:1)

  

换句话说,如果您正在编写TryFoo,您能保证吗?   异常无法逃脱   这样写吗?

不,我不会在我的TryFoo方法中吞下异常。 Foo依赖于TryFoo,而不是相反。既然你提到了通用字典,它有一个GetValue和一个TryGetValue方法,我会按如下方式编写我的字典方法:

public bool TryGetValue(T key, out U value)
{
    IList<KeyValue> kvCollection = internalArray[key.GetHashCode() % internalArray.Length];
    for(KeyValue kv in kvCollection)
    {
        if(kv.Key == key)
        {
            value = kv.Value;
            return true;
        }
    }
    value = default(U);
    return false;
}

public U GetValue(T key)
{
    U value;
    if (TryGetValue(key, out value))
    {
        return value;
    }
    throw new KeyNotFoundException(key);
}

因此,如果TryGetValue返回false,则GetValue依赖于TryGetValue并抛出异常。这比从TryGetValue调用GetValue并吞下产生的异常要好得多。

答案 8 :(得分:1)

  

但是如果TryFoo允许一些异常逃脱,那么它真正说的是“我不会抛出任何我知道如何处理的异常”,然后是合同的全部概念“我不会抛出异常”被扔出窗外。

我想你自己在这里说过:)

TryXXX应该只保证它不会抛出它知道如何处理的异常。如果它不知道如何处理给定的异常,为什么要抓住它呢?同样适合你 - 如果你不知道该怎么做,你为什么要抓住它? BadAllocExceptions例如,(通常)没有太多关于这些,除了在主try / catch块中捕获它们显示一些错误消息并尝试正常关闭应用程序..

答案 9 :(得分:1)

在您的示例中捕获和吞咽所有异常通常是一个坏主意。一些例外情况,尤其是ThreadAbortExceptionOutOfMemoryException,不应该被吞下。实际上,第一个不能被吞下,并且会在你的阻挡块结束时自动被重新抛出,但仍然。

菲尔·哈克(Phil Haack)有一个关于这个话题的blog entry

答案 10 :(得分:1)

首先要记住一个例外首先意味着什么:

异常意味着您的方法无法按其名称所做的那样执行。

我们首先实现TryFoo模式的原因是抛出异常是昂贵的,另外,如果你要求调试器在首次抛出异常时中断,它可以是非常讨厌。因此,如果您的方法具有一种特别常见的故障模式,例如Int32.Parse,当它被赋予无效输入时抛出ArgumentException,则会出现问题。

TryFoo模式背后的想法是,您的方法应该能够指示特别常见的失败模式而不会抛出异常。因此,您应该从不在这些条款中实施它:

bool TryFoo() {
    try {
        Foo();
        return true;
    }
    catch (SomeKindOfException) {
        return false;
    }
}

因为这首先颠覆了TryFoo模式的整个目的。

就抛出异常而言,答案是,是的,有时您的TryFoo方法需要抛出异常。具体来说:当方法因您期望的特别常见的原因而失败时。此处的示例可能是OutOfMemoryException,或堆栈溢出,或未部署的预期程序集,或外部Web服务不可用。这些失败模式表明某些事情需要引起注意,不应忽视。

答案 11 :(得分:0)

我曾经参加了一个很棒的演讲,他说他参与了BCL的异常处理策略。不幸的是我忘记了他的名字,我找不到我的笔记。

他描述了这个策略:

  1. 方法名称必须是描述方法所采取行动的动词。
  2. 如果名称描述的操作因任何原因未发生,则必须抛出异常。
  3. 应尽可能提供一种测试和避免即将发生的例外的方法。例如。如果文件不存在,调用File.Open(filename)将抛出异常,但是首先调用File.Exists(filename)会让你避免(大部分时间)。
  4. 如果有可证明的原因(例如,在常见情况下的表现),可以添加额外的方法,调用TryXXX,其中XXX是原始方法的名称。此方法应该能够处理单一常见故障模式,并且必须返回一个布尔值来指示成功或失败。
  5. 这里有趣的一点是#4。我清楚地记得他说明了指南的单一故障模式部分。其他失败仍然会引发异常。

    顺便提一下,他还说CLR团队告诉他.Net异常缓慢的原因是因为它们是在SEH之上实现的。他们还表示没有特别需要以这种方式实施(除了权宜之计),如果他们进入真正客户的真正性能问题的前10名,他们会考虑重新实施它们更快!