【问题标题】:What is the preferred method for handling unexpected enum values?处理意外枚举值的首选方法是什么?
【发布时间】:2010-10-11 19:42:07
【问题描述】:

假设我们有一个接受枚举值的方法。在此方法检查该值是否有效后,它会switches 覆盖可能的值。所以问题是,在验证值范围之后处理意外值的首选方法是什么?

例如:

enum Mood { Happy, Sad }

public void PrintMood(Mood mood)
{
    if (!Enum.IsDefined(typeof(Mood), mood))
    {
        throw new ArgumentOutOfRangeException("mood");
    }

    switch (mood)
    {
        case Happy: Console.WriteLine("I am happy"); break;
        case Sad:   Console.WriteLine("I am sad"); break;
        default: // what should we do here?
    }

处理default 案件的首选方法是什么?

  • 留言// can never happen
  • Debug.Fail()(或Debug.Assert(false)
  • throw new NotImplementedException()(或任何其他例外)
  • 我没想到的其他方式

【问题讨论】:

  • 我根据自己的观点发布答案。如果您有不同的意见,或支持这一观点的不同案例,请分享您的知识。该问题的目的是收集有关该主题的想法,以帮助其他人(和我)面临这种情况。
  • @Oscar:恕我直言,这应该是一个与语言无关的问题。人们可以在这里找到两种语言的答案。
  • 枚举验证对我来说很奇怪。我不使用java,可能就是这个原因。您能否描述一下该代码可能引发 ArgumentOutOfRangeException 的情况?
  • @DK:在 Java 中这永远不会发生,但在 C# 中却可能发生,因为枚举可以来回转换为整数。例如,像 PrintMood((Mood)10) 这样的调用在 C# 中是有效的,但所传递的值就方法而言是无效的。
  • 反对者:欢迎 cmets!

标签: c# java .net language-agnostic enums


【解决方案1】:

我猜上面的大部分答案都是正确的,但我不确定是否正确。

正确的答案是,你很少切换 OO 语言,这表明你做错了 OO。在这种情况下,它完美地表明您的 Enum 类存在问题。

您应该只调用 Console.WriteLine(mood.moodMessage()),并为每个状态定义 moodMessage。

如果添加了一个新状态——你的所有代码都应该自动适应,不会失败、抛出异常或需要更改。

编辑:对评论的回应。

在您的示例中,要成为“良好的 OO”,文件模式的功能将由 FileMode 对象控制。它可以包含一个委托对象,其“打开、读取、写入...”操作对于每个 FileMode 都不同,因此 File.open("name", FileMode.Create) 可以实现为(抱歉对不熟悉API):

open(String name, FileMode mode) {
    // May throw an exception if, for instance, mode is Open and file doesn't exist
    // May also create the file depending on Mode
    FileHandle fh = mode.getHandle(name);
    ... code to actually open fh here...
    // Let Truncate and append do their special handling
    mode.setPosition(fh);
}

这比尝试使用开关来做这件事要简洁得多......(顺便说一下,这些方法既是包私有的,也可能委托给“模式”类)

当 OO 做得好时,每一个方法看起来都像是几行真正易懂、简单的代码——太简单了。你总是觉得有一个大杂乱的“奶酪核”把所有的小玉米片放在一起,但你永远找不到它——一直都是玉米片……

【讨论】:

  • 就像其他所有东西一样,正确使用枚举是很好的。例如,如果我正在设计一个用于文件处理的 API,我将创建 File.Open(string filename, FileMode mode),其中 FileMode 是一个简单的值枚举,没有附加任何行为。我没有看到这种“做错了OO”。
  • 哇,我没说枚举不好!我喜欢类型安全的枚举!--而且你在那里指定的用法很棒。问题在于 switch 语句。这表明应该将一个行为移到枚举中。
  • 对不起,如果我误解了,但问题仍然存在:你如何在没有 switch 语句的情况下实现File.Open?谈到我自己,我不会给 FileMode 对象一个打开文件的方法,因为它根本不属于那里。作为替代方案,您有什么建议?
  • FileMode,作为一个与文件密切相关的枚举,实际上可能包含代码。更有可能它应该包含一个包级委托对象,该对象具有以给定模式打开文件的代码。
  • 枚举上的开关总是可以被对委托函数的调用替换——它们是完全相同的构造的转换。 OO 没有什么神奇之处,它只是让你组织你的代码,这样应该在一起的代码才是。
【解决方案2】:

我更喜欢throw new NotImplementedException("Unhandled Mood: " + mood)。重点是以后枚举可能会发生变化,这个方法可能不会相应更新。抛出异常似乎是最安全的方法。

我不喜欢Debug.Fail() 方法,因为该方法可能是库的一部分,并且新值可能不会在调试模式下进行测试。在这种情况下,使用该库的其他应用程序可能会面临奇怪的运行时行为,而在引发异常的情况下,错误将立即被知晓。

注意:NotImplementedException 存在于 commons.lang 中。

【讨论】:

  • @SnOrfus:为什么?这是一个完全有效的答案。
  • @SnOrfus:将其写在原始帖子中不允许人们对其进行投票。通过将其编写为单独的答案,社区可以独立于问题对答案进行投票,最后最好的答案将获得最多的选票(无论谁回答)。
  • 我选择这个作为接受的答案,因为目前它是最高票数。
  • @JimG。 Commons Lang 有它:commons.apache.org/proper/commons-lang/javadocs/api-2.0/org/… 或者你可以抛出 UnsupportedOperationException。参考:stackoverflow.com/a/2329389
【解决方案3】:

在 Java 中,标准方法是抛出 AssertionError,原因有两个:

  1. 这可确保即使 asserts 被禁用,也会引发错误。
  2. 您断言没有其他枚举值,因此 AssertionErrorNotImplementedException 更好地记录您的假设(Java 无论如何都没有)。

【讨论】:

  • 但是 AssertionError 扩展了 java.lang.Error,而不是 java.lang.Exception。这意味着代码中的其他层将没有机会捕获异常并从中恢复。 (从技术上讲他们可以,但实际上大多数人不会(也不应该)捕获 java.lang.Error。)
  • 如果您已经检查过它是否有效,那么 AssertionError 应该永远不会发生(也就是说,毕竟,您要断言的内容)。如果真的发生了,事情就大错特错了,我认为试图抓住它是没有意义的。但我确实看到有时让它可被捕获可能会很有用。
  • 另请参阅此问题中的讨论:stackoverflow.com/questions/398953/…
  • 但“有效”并不一定意味着“已处理”。它是枚举的有效值(嗯,在 Java 中,这是在编译时保证的,所以我们不需要检查),但它可能是开发方法时不存在的新添加。我不喜欢在这种情况下抛出错误。
  • 在这种情况下,您需要一个编译时警告。这个答案的cmets:stackoverflow.com/questions/577943/…表示Eclipse可以给出这样的警告,但不幸的是它不是标准的。
【解决方案4】:

我的意见是,由于这是一个程序员错误,您应该对其进行断言或抛出 RuntimException(Java 或其他语言的等价物)。我有自己的 UnhandledEnumException,它从我用于此的 RuntimeException 扩展而来。

【讨论】:

    【解决方案5】:

    正确的程序响应应该是,以使开发人员能够轻松发现问题。 mmyersJaredPar 都提供了很好的方法来做到这一点。

    为什么会死?这似乎太极端了!

    原因是如果您没有正确处理枚举值而只是失败,那么您将程序置于意外状态。一旦你处于一个意想不到的状态,谁知道发生了什么。这可能会导致不良数据、难以追踪的错误,甚至是安全漏洞。

    此外,如果程序死掉了,那么您在 QA 中发现它的机会就会大得多,因此它甚至不会被淘汰。

    【讨论】:

    • 是的,看起来太极端了!从应用程序可以从错误中恢复的意义上来说,抛出异常还不够吗?如果方法不是很重要怎么办?失败案例仍会在 QA 中捕获,因为它们会产生与预期不同的结果。
    • 很好,你从错误中恢复过来,但现在你不知道你的程序处于什么状态。正如我所说,这将导致其他更微妙的问题。 QA 会看到这些细微的错误并报告它们,而问题的根源并没有得到解决。
    • 从错误中恢复的不是,而是调用层次结构中的更高层。这是一个重要的区别恕我直言。如果错误被记录(应该是),一个好的错误消息将直接导致根本原因。
    • 在大多数情况下,程序(我指的是调用程序)无法从错误中正确恢复。如果程序知道异常并正确捕获/处理它,那么为什么它会传递意外值?程序死掉比较好。
    • 非常好,但我很惊讶这对我来说听起来多么合理。
    【解决方案6】:

    对于我的代码库中的几乎每个 switch 语句,我都有以下默认情况

    switch( value ) { 
    ...
    default:
      Contract.InvalidEnumValue(value);
    }
    

    该方法将在检测到错误时抛出一个详细说明枚举值的异常。

    public static void InvalidEnumValue<T>(T value) where T: struct
    {
        ThrowIfFalse(typeof(T).IsEnum, "Expected an enum type");
        Violation("Invalid Enum value of Type {0} : {1}", new object[] { typeof(T).Name, value });
    }
    

    【讨论】:

    • 但该值可能是有效的,即使该方法尚未更新以反映它。我认为“无效值”消息在这里有点误导。
    • @Hosam,但编写的代码并不期望该值,这就是我选择该名称的原因。我想“意外的价值”会起作用,但它的目的是捕捉到该价值确实不是程序员想要的
    • @JaredPar:很公平。我更多地考虑“未处理的价值”,这很清楚这是一个未处理的案例(恕我直言)。
    • 如果该值有效,那么单元测试应该覆盖该代码——尤其是在方法更新时。
    • @SnOrfus:是的,但在现实生活中你可能会忘记更新单元测试(或者你可能没有时间去做)。
    【解决方案7】:

    对于 C#,值得了解的是 Enum.IsDefined() is dangerous。你不能像现在这样依赖它。得到不符合预期值的东西是抛出异常并大声死亡的好例子。

    在 Java 中,情况有所不同,因为枚举是类而不是整数,因此您确实无法获得意外值(除非枚举已更新而您的 switch 语句未更新),这是我更喜欢 Java 枚举的一个重要原因。您还必须满足空值。但是得到一个你不认识的非空情况也是抛出异常的好例子。

    【讨论】:

    • 我当然更喜欢 Java 枚举而不是 C#。您能否详细说明在无法识别值的情况下您会抛出哪个异常?
    • 在 Java 中,如果我得到一个我不认识的枚举值,我会抛出一个 IllegalStateException。 IllegalArgumentException 没问题,但对我来说它更像是非法状态。
    【解决方案8】:

    您可以跟踪默认调用传递的枚举的值。抛出异常是可以的,但在大型应用程序中,您的代码会在多个地方不关心枚举的其他值。
    因此,除非您确定代码打算处理枚举的所有可能值,否则您必须稍后返回并删除异常。

    【讨论】:

      【解决方案9】:

      这是证明测试驱动开发为何如此重要的问题之一。

      在这种情况下,我会选择 NotSupportedException,因为实际上该值未处理,因此不受支持。 NotImplementedException 提供了更多的想法:“这还没有完成”;)

      调用代码应该能够处理这种情况,并且可以创建单元测试来轻松测试这种情况。

      【讨论】:

      • 这是一个很好的观点。但有时您会为有效值抛出 NotSupportedException,例如在使用 FileMode.Write 创建 StreamReader 时。我在想正在讨论的案件已经足够不同了,不应该像其他案件一样处理。
      【解决方案10】:

      称之为意见或偏好,但枚举背后的想法是它代表可能值的完整列表。如果在代码中传递“意外值”,则枚举(或枚举背后的目的)不是最新的。我个人的偏好是每个枚举都带有一个未定义的默认分配。鉴于枚举是一个已定义的列表,它永远不会与您的消费代码过时。

      就我而言,如果您的函数获得意外值或 Undefined 时该怎么办,似乎不可能给出通用答案。对我来说,这取决于评估枚举值的原因的上下文:是代码执行应该停止的情况,还是可以使用默认值?

      【讨论】:

        【解决方案11】:

        提供有效输入是调用函数的责任,并且隐含地不在枚举中的任何内容都是无效的(实用程序员似乎暗示了这一点)。也就是说,这意味着每次更改枚举时,都必须更改接受它作为输入的所有代码(以及将其作为输出的一些代码)。但无论如何,这可能是真的。如果您有一个经常更改的枚举,则考虑到枚举通常是编译时实体,您可能应该使用枚举以外的其他东西。

        【讨论】:

        • 我完全同意,但在现实世界中,更新枚举时可能会遗漏一些方法,这就是我问这个问题的原因。在这种情况下你会怎么做?
        【解决方案12】:

        我通常尝试定义未定义的值(0):

        enum Mood { Undefined = 0, Happy, Sad }
        

        这样我总能说:

        switch (mood)
        {
            case Happy: Console.WriteLine("I am happy"); break;
            case Sad:   Console.WriteLine("I am sad"); break;
            case Undefined: // handle undefined case
            default: // at this point it is obvious that there is an unknown problem
                     // -> throw -> die :-)
        }
        

        至少我通常是这样做的。

        【讨论】:

        • 虽然我记得在 MSDN 的某处读到应该为枚举创建一个“未定义”值,但我发现这对于一些不应该具有此类值的枚举(例如 FileMode)来说是个问题。除非需要,否则它会强制开发人员在每次编写方法时检查它。
        • 而默认情况还是需要处理的,这就是问题的重点。
        • 好吧,我在评论中提到过——扔了就死。我会抛出我自己的从 Exception 类派生的异常。
        • 对不起@David,我错过了。长评论有点误导。 :) 我把它分成两行。希望你不要介意。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-05-25
        • 1970-01-01
        • 1970-01-01
        • 2013-07-27
        • 2017-01-27
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多