【问题标题】:Should I throw my own ArgumentOutOfRangeException or let one bubble up from below?我应该抛出我自己的 ArgumentOutOfRangeException 还是让一个从下面冒泡?
【发布时间】:2010-03-26 20:14:00
【问题描述】:

我有一个包装列表的类

我有通过索引方法获取值:

    public RenderedImageInfo GetValue(int index)
    {
        list[index].LastRetrieved = DateTime.Now;
        return list[index];
    }

如果用户请求的索引超出范围,这将抛出 ArgumentOutOfRangeException 。

我应该让这种情况发生还是检查一下并自己扔掉?即

    public RenderedImageInfo GetValue(int index)
    {
        if (index >= list.Count)
        {
            throw new ArgumentOutOfRangeException("index");
        }
        list[index].LastRetrieved = DateTime.Now;
        return list[index];
    }

在第一个场景中,用户会从内部列表中获得一个异常,这打破了我的 OOP 目标,即用户不需要了解底层对象。

但在第二种情况下,我感觉好像我在添加冗余代码。

编辑:
现在我想起来了,第三种情况呢,我捕获内部异常,修改它,然后重新抛出它?

【问题讨论】:

  • 我强烈建议您反对您的第三种情况。
  • @TrueWill,你能说明原因吗?
  • 不好修改现有的异常 - 这真的会是意想不到的行为(更不用说没有反射是不可能的,因为 ParamName 没有属性设置器):msdn.microsoft.com/en-us/library/…
  • 您可以合法地对异常进行的唯一“修改”是 (a) 包装它,(b) 使用异常上的 .Data 属性将附加信息传递到调用堆栈。

标签: c# .net exception exception-handling


【解决方案1】:

你应该扔自己的,有几个原因:

  1. 您可以显式设置appropriate parameter name in the constructor。这样,异常具有超出范围的 Argument 的适当参数信息。
  2. (次要)就您的用户而言,内部列表的异常将具有无效的堆栈跟踪。通过构造一个新的异常,您可以获得堆栈跟踪,显示您的方法是不合适的,这将更便于您的用户调试。

至于捕获和修改内部异常 - 我不一定会推荐这个。如果异常是额外信息可能有用的异常,您应该使用新异常的InnerException 来传播此信息。

在此示例 (ArgumentOutOfRangeException) 中,存在内部列表这一事实应作为实现细节保留,您的用户没有理由看到该信息。

【讨论】:

  • 我决定,如果我确实扔了自己的,或者重新扔了较低的,我至少会添加参数名称(虽然在这种方法中只有一个,所以它是一种给定的参数是有罪的)
  • @Neil:无法将参数“添加”到现有异常中。该列表将包含一个,但我不会依赖它保持不变...话虽如此,我永远不会修改处理程序中的异常...
  • @Reed:我想您可能会争辩说,自己进行范围检查并抛出自己的异常会更干净。但是,在示例代码中,变量名称未更改,并且使用 List 的事实并不是特别危险的共享。考虑您正在实现索引器并且基础集合是 Dictionary 的情况;您必须搜索两次,首先使用包含,然后使用索引器。这似乎是一笔很大的开销,而且收益甚微。
  • @Steven:您假设 List 的索引器使用参数名称“index”(请记住,由 List 实现来处理此问题,并且可以在不破坏 API 兼容性的情况下根据您进行更改。. .) 不过,您有点证明了我的观点——如果您决定从 List 更改为其他集合,那么让异常冒泡会改变您的 API 的异常行为。这应该是一个完整的实现细节......
  • @Reed:我并不否认这种区别,我只是看不出它有什么实际区别。该异常将用于诊断意外故障,而不是用于编程分支。
【解决方案2】:

如果您使用的是 .NET 2.0 或更高版本,则可以使用the inner exception,如下所示:

public RenderedImageInfo GetValue(int index)
{
    try {
        list[index].LastRetrieved = DateTime.Now;
        return list[index];
    } catch (ArgumentOutOfRangeException ex) {
        throw new ArgumentOutOfRangeException("index", ex);
    }
}

这样,您的用户将主要看到您的异常,但如果您(或他们)愿意,也可以深入挖掘。

编辑:我现在看到 InnerException 自 .NET 1.0 以来就已受支持,只是那个构造函数是新的。我看不到在 2.0 之前在 .NET 中为 ArgumentOutOfRangeException 实际设置 InnerException 的方法。他们只是忘记了,还是我上面写的代码违背了预期用途?

【讨论】:

  • 当我说在异常中添加/修改参数名称时,我想这就是我的意思。但总的来说,我认为里德只是扔一个新的答案更好。 +1 以确保完整性。
  • 这不是一个好的通用解决方案。当您需要检查一个 List 然后另一个时,您会怎么做?
  • 明确地说,可以通过将"index" 替换为随着代码进行而设置为不同值的变量来使更通用,例如它是 index1在我们使用 index1 之前,以此类推。是不是更清楚了?
【解决方案3】:

...打破用户不需要了解底层对象的 m[y] OOP 目标。

抛出相同的异常对封装没有任何作用。您的(隐含/记录的 - 因为我们没有检查异常或 DbC)合同声明在这种情况下您将抛出 ArgumentOutOfRange 异常。该异常是如何生成的,以及它的堆栈跟踪是什么样的,与调用者无关。

如果您将内部实现移动到不会抛出 ArgumentOutOfRange 异常的东西,那么您需要自己抛出一个来履行您的合同(或对合同进行重大更改) .

堆栈跟踪(和参数名称)用于调试人员 - 不用于编程访问。除非有一些安全问题,否则不必担心让它们“泄漏”。

顺便说一句,包装异常的建议(您可能会想到)来自更抽象的场景。考虑一个IAuthenticationService,如果用户不能Login,则抛出,

如果同时存在LdapAuthenticationServiceDatabaseAuthenticationService 实现,那么您必须同时捕获LdapDirectoryExceptionSqlException 才能确定登录失败。当第三个实现完成时,您还需要添加它的特定异常类型。通过所有实现者将他们的特定异常包装在FailedAuthenticationException 中,您只需要担心单一类型。

不过,将原始异常包含在 InnerException 中仍然是个好主意,因为它有助于调试。

我希望您能看到这种情况和您的情况之间的区别 - 在您的情况下,您只是抛出相同的异常类型,所以没有区别。

话虽如此,如果它是为了一个图书馆——那么我会检查它并扔掉。但不是为了 OOP 纯度——而是因为它使合同明确且不太可能被无意更改。如果我使用 [T|B]DD,那么我只需为它编写一个测试,然后让 List 抛出 - 测试使合同明确。

【讨论】:

    【解决方案4】:

    在这种情况下,让异常冒泡很好。

    编辑

    有人要求我更新我的答案以包含一些回复...

    可以说,捕获异常并将其重新抛出为内部更健壮,因为它使您免受更改并创建更一致的界面。话虽如此,但在开发人员可以访问所有代码的环境中,收益可能非常小——几乎没有。此外,我们获得的任何小利益都不是免费的。额外的错误处理代码增加了复杂性,并且必须对其自身进行测试以确保其正常工作。它还增加了在维护期间破坏该方法的风险,因为该方法的基本功能可能会被所有防弹措施所掩盖。

    那么,值得吗?这取决于。它归结为成本/收益分析,只能针对特定环境进行。对于像 AlphaFS 这样的公共 API,收益可能远远超过成本。对于不是为重用而设计的代码的内部使用,它可能不是。那是我最初答案的背景,我仍然支持。此外,正如 Hightechrider 指出的那样,可能还有其他技术,例如编码合同,它们产生的好处大致相同,但成本更低,从而改变了等式,有利于稳健性。

    与往常一样,我对合理的分歧持开放态度。不过,我建议我们都尽量避免一刀切的想法。

    编辑

    为了清楚说明,当我说错误处理代码容易出错时,我并没有发疯,请查看原始问题并告诉我您是否可以发现错误。如果你愿意,我会提供 +1。

    【讨论】:

    • Reed 提出了一些有趣的观点,说明为什么有时您想抛出自己的异常,但如果反对者解释为什么它不好在这种情况下.
    • 我没有投反对票,但我认为当你的回答没有任何理由时,你向投反对票的人询问理由有点讽刺。 为什么在这种情况下可以吗?
    • 好吧,如果反对者问我为什么,我将有机会回答。事实上,我已经通过我对其他人的回答做出了回答。总结是没有好处,但有成本。无论哪种方式,都会抛出相同的异常,只是堆栈跟踪的深度不同。为什么要无缘无故地使代码复杂化?
    • +1。 “为什么无缘无故地使代码复杂化” - 我同意。抛出 ArgumentNullException 而不是取消引用空引用,以避免抛出 NullReferenceException。但是如果将引用传递给较低级别​​的库,我不会打扰。
    • 我会更新您的答案以解释简单性是好的:更多的代码通常意味着更多的错误,并且更难真正看到一个方法的作用。如果这个类是供内部使用的,它可能就可以了。如果这是 .NET 4,这里的代码契约会更好,因为它有助于记录方法所期望的内容,并且可以启用对参数的编译时检查。
    猜你喜欢
    • 1970-01-01
    • 2015-08-27
    • 1970-01-01
    • 1970-01-01
    • 2012-09-02
    • 2012-01-15
    • 1970-01-01
    • 2011-04-27
    • 1970-01-01
    相关资源
    最近更新 更多