【问题标题】:What type of Exception should I throw when an unknown value is passed into a switch statement将未知值传递给 switch 语句时应该抛出什么类型的异常
【发布时间】:2012-10-16 14:04:19
【问题描述】:

编辑 1

更新为使枚举不是方法的参数...

问题

这种类型的问题经常出现在 switch 语句中的枚举中。在示例代码中,开发者已经计算了程序当前使用的所有国家,但是如果将另一个国家添加到 Country 枚举中,则应该抛出异常。我的问题是,应该抛出什么类型的异常?

示例代码:

enum Country
{
    UnitedStates, Mexico,
}

public string GetCallingCode(Guid countryId){
    var country = GetCountry(countryId);
    switch (country)
    {
        case Country.UnitedStates:
            return "1";
            break;
        case Country.Mexico:
            return "52";
            break;
        default:
            // What to throw here
        break;
    }
}

看过

  • NotImplemented请求的方法或操作未实现时抛出的异常。
  • NotSupported 基类中不支持某些方法,期望这些方法将在派生类中实现。派生类可能只实现基类方法的子集,并为不受支持的方法抛出 NotSupportedException。
    对于对象有时可以执行请求的操作,而对象状态决定该操作是否可以执行的场景,请参见 InvalidOperationException。
  • InvalidOperation 用于由于参数无效以外的原因导致方法调用失败的情况。

我的猜测是 NotImplemented 或 Invalid Operation。我应该使用哪一个?有人有更好的选择吗(我知道滚动你自己总是一个选择)

【问题讨论】:

  • 我会抛出自定义 UnknownCountryException。或者至少将 ArgumentOutOfRangeException 添加到您的列表中..
  • 我对参数异常的唯一问题是它们通常用于方法参数。在 this 特定问题的情况下,它是适用的,但对于一般的 switch/case 用法,它可能根本不会检查方法的参数。
  • @ChrisSinclair 同意 Chris,这就是我更新示例代码的原因...
  • @Daryl:我编辑了我的答案。我认为答案中有两个不同的案例正在讨论。我认为最重要的是,问题/代码本身并不是一个好的做法,可以通过其他更好的方式来处理。

标签: c# exception-handling


【解决方案1】:

我会选择ArgumentException,因为该协议无效。

编辑:http://msdn.microsoft.com/en-us/library/system.argumentexception%28v=vs.71%29.aspx

还有InvalidEnumArgumentException,可能更准确地描述了问题,但是,我之前没有看到有人使用过。

【讨论】:

  • 有趣的是,MSDN 似乎将InvalidEnumArgumentException 用于此类情况。但事实上它继承自ArgumentException 并且甚至被认为是一个名字,这让我质疑将它用于非争论的情况。 +1 不管找到那个小宝石并且绝对适合 如果枚举值是一个参数。
【解决方案2】:

一个选项是在调试模式下进行几乎方法契约检查。为美观的表单添加一个扩展方法:

[Conditional("DEBUG")]
public static bool AssertIsValid(this System.Enum value)
{
    if (!System.Enum.IsDefined(value.GetType(), value))
        throw new EnumerationValueNotSupportedException(value.GetType(), value); //custom exception
}

我认为它可能只处于调试模式,因此它可以通过您的开发/测试环境和单元测试,并且在生产中没有开销(尽管这取决于您)

public string GetCallingCode(Guid countryId)
{
    var country = GetCountry(countryId);
    country.AssertIsValid(); //throws if the country is not defined
    
    switch (country)
    {
        case Country.UnitedStates:
            return "1";
        case Country.Mexico:
            return "52";
    }
}

我建议这实际上是您的 GetCountry 方法的责任。 应该识别countryId无效并抛出异常。

无论如何,这也应该被您的单元测试捕捉到,或者以某种方式更好地处理。无论您在何处将字符串/int 转换为枚举,都应该由一个单一的方法处理,该方法反过来可以检查/抛出(就像任何 Parse 方法一样)并且有一个检查所有有效数字的单元测试。

总的来说,我不认为各种ArgumentExceptions(等)是一个好的候选者,因为有几个条件(非参数)情况。我认为如果您将检查代码移动到一个位置,您不妨抛出自己的异常,该异常可以准确地传达给任何正在监听的开发人员。

编辑:考虑到讨论,我认为这里有两个特殊情况。

案例 1:将底层类型转换为等效枚举

如果您的方法采用某种类型的输入数据(字符串、整数、Guid?),您执行转换为枚举的代码应该验证您有一个可用的实际 枚举。这是我在上面的答案中发布的情况。在这种情况下,可能会抛出您自己的异常或InvalidEnumArgumentException

这应该像任何标准输入验证一样对待。在您系统的某个地方,您提供了垃圾输入,因此请像处理任何其他解析机制一样处理它。

var country = GetCountry(countryId);

switch (country)
{
    case Country.UnitedStates:
        return "1";
    case Country.Mexico:
        return "52";
}

private Country GetCountry(Guid countryId)
{
    //get country by ID
    
    if (couldNotFindCountry)
        throw new EnumerationValueNotSupportedException(.... // or InvalidEnumArgumentException

    return parsedCountry;
}

编辑:当然,编译器要求你的方法抛出/返回,所以不太确定你应该在这里做什么。我想这取决于你。如果确实发生了这种情况,那可能是一个愚蠢的异常(下面的案例 2),因为您通过了输入验证然而没有更新开关/案例来处理新值,所以也许它应该 @ 987654331@;

案例 2:添加一个新的枚举值,您的代码中的 switch/case 块没有处理该值

如果您是代码的所有者,这属于 Eric Lippert 在@NominSim 的回答中描述的“Boneheaded”异常。虽然这实际上不会导致异常同时使程序处于异常/无效状态。

最好的方法可能是在您执行 switch/case(或类似操作)的任何地方针对枚举运行,您应该考虑编写一个单元测试,自动针对 运行该方法>所有定义的枚举值。因此,如果您懒惰或不小心错过了某个块,您的单元测试会警告您没有更新方法来说明枚举列表中的更改。

最后,如果您的枚举来自您没有意识到他们更新了值的第 3 方,您应该编写一个快速的单元测试来验证您的所有预期值。因此,如果您在编写程序时检查了UnitedStatesMexico,那么您的单元测试应该只是这些值的开关/案例块并抛出异常,否则会在/如果它们最终添加Canada 时警告您。当更新第 3 方库后测试失败时,您就知道必须在哪些/在哪里进行更改才能兼容。

所以在这个“案例 2”中,您应该抛出任何您想要的旧异常,因为只要它准确地与 或您的单元测试的消费者沟通,它就会由您的单元测试处理正是缺少的东西。


在任何一种情况下,我都不认为 switch/case 代码应该过多地关心无效输入并且不在那里抛出异常。它们应该在设计时抛出(通过单元测试)或在验证/解析输入时抛出(因此抛出适当的“解析/验证”异常)


编辑:我偶然发现post from Eric Lippert 讨论 C# 编译器如何检测具有返回值的方法是否在没有返回的情况下达到其“终点”。编译器擅长保证端点有时是不可达的,但在上述情况下,我们开发人员知道它是不可达的(除了上面提到的BoneheadedExceptions 发挥作用的情况)。

没有讨论过(至少我看到了)作为开发人员应该做些什么来解决这些情况。编译器要求您提供返回值或抛出异常,即使您知道它永远不会到达该代码。在这种情况下,谷歌搜索并没有神奇地出现一些可以利用的异常(尽管我无法找出好的搜索词),我宁愿抛出一个异常并被告知我的假设它不能到达终点是不正确的,而不是返回 一些值,这可能不会通知我问题或导致不需要的行为。也许某种UnexpectedCodePathFailedToReturnValueException 在这种情况下最有效。当我有时间时,我会做更多的挖掘工作,也许会在programmers 上发布一个问题以引起一些讨论。

【讨论】:

  • 嗯...也许我会创建一个 HeyYouDummyDeveloperYouTotallForgotToUpdateThisSwitchStatmentException? :) 怎么想?尽管我在开玩笑,但这就是我抛出异常的真正目的。
  • @Daryl 我在答案的末尾添加了另一个关于这个概念的有趣想法。 Eric Lippert 很好地阅读了编译器检测代码路径和返回值的方法的端点的概念,我认为这里的情况非常适用。
【解决方案3】:

在您列出的例外情况中,只有 InvalidOperationException 适合您的情况。我会考虑使用这个,或者ArgumentException或更具体的ArgumentOutOfRangeException,因为你的switch值是作为参数提供的。

或者,如你所说,自己动手。

编辑:根据您更新的问题,如果您想使用框架异常,我建议InvalidOperationException。但是,对于这种更通用的情况,我肯定更愿意自己动手​​——你不能保证InvalidOperationException 不会在调用堆栈的其他地方被捕获(可能被框架本身捕获!),所以使用你自己的异常类型更加健壮。

【讨论】:

    【解决方案4】:

    如果您正在使用的值纯粹是对象当前状态的产物,我会使用InvalidOperationException。正如它所说:

    当方法调用对于对象的当前状态无效时引发的异常。

    即使您更新了问题,由于您无法正确处理的特定值来自传递给它的参数,我仍然会使用ArgumentException - 您可以在错误消息中解释您的信息 派生的参数与您可以处理的任何内容都不匹配。


    对于NotImplementedExceptionNotSupportedException 的期望是,无论调用者做什么,他们都无法纠正这种情况。而 ArgumentExceptionInvalidOperationException 是线索,如果调用者将使用不同的参数,或将对象转换到另一个状态(分别),调用可能会起作用。

    【讨论】:

    • 我喜欢 NotImplemented 所传达的想法,即并非一切都能奏效......好点。
    【解决方案5】:

    就个人而言,我认为这根本不适合任何异常。如果添加 Country,则应在 switch 语句中添加 case。代码不应因为您向枚举添加值而中断。

    Eric Lippert 关于何时使用异常的 article 将您要查找的异常类型分类为:(请原谅它不是我的措辞)

    愚蠢的异常是你自己的错误,你可以阻止它们,因此它们是你代码中的错误你不应该抓住它们;这样做会在您的代码中隐藏一个错误。相反,您应该编写您的代码,以便一开始就不可能发生异常,因此不需要被捕获。

    【讨论】:

    • 同意你应该添加case,但是如果其他人5年后添加枚举值,你认为他们会知道添加case吗?
    • 爱 Eric Lippert,并同意他的说法,但如果例如,该程序用于自动拨号一个国家而您不包括例外情况,您会怎么做?返回 0? -1?很可能以后会抛出更严重的异常,或者当你想要中国时你最终会打电话给肯尼亚......
    • @Daryl 老实说,如果我正在编写此代码,我可能会将国家/地区代码(和任何其他信息)封装在单独的结构或对象中,以便添加国家/地区涉及添加其他必要元素,例如国家/地区代码.这样更进一步,如果有人想添加一个国家,他们也需要添加国家代码。如果您像您说的那样抛出异常,则会发生相同的问题(一个人可以添加一个国家/地区而无需添加案例)但程序只会在他们身上崩溃(这不是好)最好把IMO 在一个位置“添加”所有内容。
    • 完全同意。但是,如果枚举由第三方 dll 拥有怎么办???与其专注于通过一些假设来创建可能的最佳解决方案,不如专注于问题本身。 +1 :)
    • 你是在断章取义地引用这句话。 不应该捕获异常是对的,但问题是应该抛出什么异常?并且肯定应该抛出一个异常,否则您无法确定自己错过了更新的地点!
    【解决方案6】:

    不可能传递另一个值,因为您的枚举将可能的值限制为您处理的值。所以你不需要任何例外。

    【讨论】:

    • 我认为问题在于如果他们添加一个新的枚举值,这将处理这种情况,而不是经历一个空的默认值并继续处于错误状态。
    • 不正确。枚举派生自数字类型;您可以毫无错误地致电GetCallingCode((Country)42)。此外,枚举可能会在以后扩展并且不支持新值;这是对代码进行未来验证的常用方法。
    • @ChrisSinclair 这或多或少是我的想法。
    猜你喜欢
    • 2011-05-06
    • 1970-01-01
    • 1970-01-01
    • 2011-02-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-11-08
    • 1970-01-01
    相关资源
    最近更新 更多