代码之家  ›  专栏  ›  技术社区  ›  Jim Mischel

TryFoo是否应该引发异常?

  •  8
  • Jim Mischel  · 技术社区  · 15 年前

    .NET框架中的一个常见模式是Tryxx模式(我不知道这是否是他们真正称之为的模式),在该模式中,被调用的方法尝试做一些事情,返回 True 如果成功,或 False 如果操作失败。一个好的例子是一般的 Dictionary.TryGetValue 方法。

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

    我最近遇到了两种不同的情况,即实现Tryxx模式的.NET框架方法抛出异常。参见 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;
        }
    }
    

    这很诱人,因为这保证了任何例外都不会逃脱,从而履行了隐含的合同。但它是一个昆虫藏身处。

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

    那么,你怎么看?应该 特里福 处理所有异常,还是只处理那些它期望发生的异常?你的理由是什么?

    12 回复  |  直到 12 年前
        1
  •  5
  •   ojrac    15 年前

    这要看情况而定。 TryParseFoo 应捕获空字符串或格式错误字符串等内容的所有分析异常。但也有不相关的例外( ThreadAbortException ,比如)这不应该自动安静。

    在巨蟒中,你可以通过捕捉 KeyboardInterrupt

    TryFoo Foo 根据你的意见,这太激进了。

        2
  •  5
  •   JP Alioto    15 年前

    不能保证不抛出异常。考虑一下字典上的这个小例子…

    var dic = new Dictionary<string, string>() { { "Foo", "Bar" } };
    
    string val = String.Empty;
    string key = null;
    
    dic.TryGetValue(key, out val); // oops
    

    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块处理tryxx失败案例(所以您没有一堆普通的日志实体)。在这种情况下,您至少要揭示bug的性质,并在开发期间识别它,并在运行时注意它。

        3
  •  3
  •   Nick Lewis    15 年前

    TryFoo 不能 它将处理无效的输入作为正常情况,而不是异常情况。也就是说,如果由于输入无效而使任务失败,则不会引发异常;如果由于任何其他原因而失败,则应引发异常。之后我的期望

        4
  •  2
  •   IRBMe    15 年前

    Uri.TryCreate 抛出一个UriFormatException,例如,正如该方法的msdn文档明确指出的那样,它绝对是合同中的一个中断:

    如果无法创建URI,则不会引发异常。

    另一个是TrydeQueue,我找不到任何文档,但它似乎也是一个bug。如果调用了方法

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

    不应引发异常,也不应设计为隐藏异常。它适用于失败是正常的和非异常的情况,并且您希望能够轻松地检测到该情况,而无需花费额外的开销和努力捕获异常。通常也会提供一个非Tryxx方法,它确实提供异常,用于异常情况。

        5
  •  1
  •   Barry Kelly    15 年前

    TryXXX try/catch 在xxx周围配对(而不是首选的,xxx是根据tryxx实现的,并抛出错误的结果),只捕获特定的预期异常。

    Tryxxx 如果有语义上不同的问题,例如程序员错误(可能在不期望的地方传递空值),则抛出。

        6
  •  1
  •   Guffa    15 年前

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

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

        7
  •  1
  •   Andrew Hare    15 年前

    TryXXX XXX 调用-方法的使用者知道他们忽略了通常会发生的任何异常。

    这很诱人,因为这保证了任何例外都不会逃脱,从而履行了隐含的合同。

    这不是完全正确的-有一些例外 可以 try/catch OutOfMemoryException StackOverflowException

        8
  •  1
  •   cwap    15 年前

    但是如果TryFoo让一些异常逃脱,那么它真正说的是,“我不会抛出任何我知道如何处理的异常”,然后合同的整个理念“我不会抛出异常”就被抛到了窗外。

    Tryxx应该只保证它不会抛出它知道如何处理的异常。如果它不知道如何处理给定的异常,为什么要捕获它?你也一样-如果你不知道该怎么做,为什么你会抓住它?badalloceexceptions例如,好吧,(通常)除了在您的主try/catch块中捕获它们之外,对它们没有太大的作用。显示一些错误消息并尝试优雅地关闭应用程序。

        9
  •  1
  •   Martin Liversage    15 年前

    像在您的示例中那样捕获和吞咽所有异常通常是一个坏主意。一些例外,尤其是 ThreadAbortException OutOfMemoryException 不应吞咽。实际上,第一个不能被吞咽,并且会在你的捕获块结束时自动重新拥有,但仍然如此。

    菲尔·哈克有一个 blog entry

        10
  •  1
  •   Juliet    15 年前

    像这样写?

    不,我不会在我的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);
    }
    

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

        11
  •  1
  •   jammycakes    12 年前

    TryFoo 首先,模式是抛出异常非常昂贵,另外,如果您要求调试器在第一次抛出异常时中断,则可以 Int32.Parse ArgumentException

    模式是您的方法应该能够指示

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

    因为这颠覆了

    就抛出异常而言,答案是,是的,有时 特里福 OutOfMemoryException 或堆栈溢出,或未部署预期的程序集,或外部Web服务不可用。这些故障模式表明有些事情需要注意,不应忽视。

        12
  •  0
  •   Swythan    15 年前

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

    1. 如果名称所描述的操作由于任何原因没有发生,则必须引发异常。
    2. 在可能的情况下,应提供一种测试和避免即将发生的异常的方法。例如,如果文件不存在,则调用file.open(文件名)将引发异常,但首先调用file.exists(文件名)将使您避免这种情况(大多数情况下)。
    3. 如果有明显的原因(例如在常见情况下的性能),可以添加一个额外的方法,调用tryxx,其中xxx是原始方法的名称。此方法应该能够处理 单一常见故障模式 必须返回一个布尔值以指示成功或失败。

    这里有趣的一点是4。我清楚地记得他说过 单一故障模式 指南的一部分。其他失败仍应引发异常。

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