【问题标题】:What is the correct exception to throw for unhandled enum values?为未处理的枚举值抛出的正确异常是什么?
【发布时间】:2012-11-18 16:04:28
【问题描述】:

这是我的other question about unhandled cases with enums 的另一个案例,建议我将其作为一个单独的问题提出。

假设我们有 SomeEnum 并有一个 switch 语句来处理它:

enum SomeEnum
{
  One,
  Two
}

void someFunc()
{
  SomeEnum value = someOtherFunc();
  switch(value)
  {
     case One:
       ... break;
     case Two:
       ... break;
     default:
         throw new ??????Exception("Unhandled value: " + value.ToString());    
  }
}

如您所见,我们处理所有可能的枚举值,但仍保留默认值,以防添加新成员时抛出异常,并且我们希望确保我们知道丢失的处理。

我的问题是:在您想要通知给定代码路径未处理/实现或不应该访问的情况下,正确的例外是什么?我们曾经使用NotImplementedException,但它似乎并不合适。我们的下一个候选人是InvalidOperationException,但这个词听起来不太对。什么是正确的,为什么?

【问题讨论】:

  • GivenCodePathIsNotHandledException
  • @AmithGeorge:但这不是争论?
  • 看起来 someOtherFunc 行为不端并返回无效枚举。 someOtherFunc 不应该是抛出异常的函数吗?它会更好地了解为什么会生成无效值。
  • @AmithGeorge:将它们视为由不同开发人员开发的两个独立组件。开发人员 B 添加了一个新的枚举返回值,但开发人员 A 不知道。在这种情况下,A 的代码会默默地忽略新的枚举值,并可能会产生难以检测的问题。例外可以避免这种情况。
  • 是的。鉴于此,someFunc 不是 NotSupported 的情况吗?虽然我想知道当someFunc 仅适用于这两个值时会发生什么。在添加第三个枚举值不会影响someFunc 的操作的情况下。在这些情况下,它仍然会抛出异常。要处理这些,您需要添加空案例处理程序。

标签: c# .net exception


【解决方案1】:

尝试使用InvalidEnumArgumentException Class

void someFunc()
{
  SomeEnum value = someOtherFunc();
  switch(value)
  {
     case One:
       ... break;
     case Two:
       ... break;
     default:
          throw new InvalidEnumArgumentException(); 
  }
}

【讨论】:

  • 我不敢相信他们将异常放入 System.ComponentModel....叹息。
  • 考虑使用重载throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(SomeEnum))。这将为您提供包含更多信息的标准消息。
  • 在这种情况下,这不是一个论点。问题中链接了这可能适用的情况。
【解决方案2】:

我认为这取决于枚举所代表的语义。

InvalidOperationException 表示对象状态时是合适的。

NotSupportedException 表示不受支持的应用程序功能时是合适的。

NotImplementedException 适用于当前未实现但可能在未来版本中实现的应用程序功能。

...

【讨论】:

  • +1,虽然我觉得你的推理最明智,但这种粒度级别需要太多思考(“这个对象状态是否?”,“这是一个特征吗?”,“这可以在未来实施吗?”,以及组合)。我们将采用 OR 对 InvalidOperationException 的更实际解释以及我的另一个问题 (InvalidEnumArgumentException) 的答案。谢谢。
【解决方案3】:

开关盒的 ReSharper 命题:

switch(parameter)
{
   default:
      throw new ArgumentOutOfRangeException("parameter");
}

但它可能不符合您的需求,在这种情况下,您可以定义一个自定义异常类型,了解在此函数中执行的操作:SomeEnumOutOfRangeException...

【讨论】:

  • 这不是争论。请查看问题中的链接。
【解决方案4】:

由于这是一个失败的内部操作(产生无效的东西),InvalidOperationException 是要走的路。

文档只是说:

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

这很合适,因为对象的当前状态导致someOtherFunc的返回值无效,因此应该首先避免调用someFunc

【讨论】:

  • 取决于 someOtherFunc() 是否从当前对象获取枚举值,我们需要一个精度
  • 我发现这个答案最实用,语义上最接近所有可能的情况,除了参数验证(在我的另一个问题中回答)。谢谢大家。
【解决方案5】:

这种情况实际上是合同失败,不一定特定于枚举。如果你不能使用代码契约,你可以创建自己的异常类型并抛出它。

case One:
   ... break;
 case Two:
   ... break;
 default:
    throw new ContractViolationException("Invalid enum");

【讨论】:

  • 我不相信这种解释 - 将收到异常的代码(someFunc() 的调用者)没有直接或间接违反任何合同。
  • @O.R.Mapper - 发生了合同违规 - 调用者也没有执行无效操作或传递无效参数。只有在 someOtherFunc 通过返回枚举类型的无效值而违反合同的情况下才会输入此代码路径。
【解决方案6】:

如果添加了一个新值而您忘记在某处处理它,则这是一个编程错误,或者 Eric Lippert 称之为Boneheaded Exception。我创建了自己的 BoneheadedException 类,每当我检测到没有 FCL 异常类型更适合的编程错误时,我都会抛出该类。

【讨论】:

  • :) +1 但在团队合作中,它并不总是与个人有关,而是与沟通或过程有关。我拒绝马上给它贴上愚蠢的标签:)
【解决方案7】:

我会说 NotImplementedException 因为您正在为未实现的枚举值抛出异常。

【讨论】:

  • 来自the docs,它说当请求的方法或操作未实现时抛出的异常。这是一个延伸,因为实际上实现了枚举开关,但它不知道如何处理至少一种情况。如果有人争辩说“或操作未实施”这一措辞涵盖了这一点,那么InvalidOperationException 也是如此,没有歧义。
  • 呃...这里被评为最没有帮助的答案。鉴于最受欢迎的答案是InvalidEnumArgumentException,并且该例外似乎完全符合 OP 的问题,您为什么觉得有必要发表这些评论?
  • 您好。我不是故意的,所以很抱歉你这样说。我一点也不觉得你的建议不好,这就是为什么我觉得有必要向潜在读者解释为什么这不是一个好的选择,而不是让他们仅仅依赖一个负分。 (我经常发现不太受欢迎的答案不一定是坏的。)下次我做这样的事情时,我会确保听起来更积极。再次,对不起,我冒犯了你。
  • 我没有被冒犯。答案是 4 岁。
【解决方案8】:

就我个人而言,我在我的项目中添加了一个自定义异常:

public class UnexpectedEnumValueException<T> : Exception
{
    public UnexpectedEnumValueException( T value )
        : base( "Value " + value + " of enum " + typeof( T ).Name + " is not supported" )
    {
    }
}

那么我可以根据需要使用它:

enum SomeEnum
{
  One,
  Two
}

void someFunc()
{
  SomeEnum value = someOtherFunc();
  switch(value)
  {
   case SomeEnum.One:
    ... break;
   case SomeEnum.Two:
    ... break;
   default:
      throw new UnexpectedEnumValueException<SomeEnum>(value);    
  }
}

这样,我可以搜索“UnexpectedEnumValueException”,例如,当我向 SomeEnum 添加新值并且我想查找所有可能受更改影响的地方时。错误消息比一般异常要清楚得多。

【讨论】:

  • 这是一个流行的答案,但请记住,自定义异常有序列化之类的陷阱。我不建议使用这种方法。
  • @SedatKapanoglu 由于这个特殊例外的简单性,这不是问题吗?还是值变量有问题?
  • @BrianMacKay 是的,值是序列化的问题,但即使没有自定义属性,您也需要拥有正确的构造函数集才能正确序列化类型本身。话虽如此,序列化的重要性在 .NET 核心中可能已经退居二线,简单的异常覆盖可能不再那么麻烦了。
【解决方案9】:

在这种情况下,我尝试使用字典而不是 switch 语句。 (一般来说,像这样的映射最好用字典来定义;我几乎认为switch 语句会自动产生代码气味,因为总是或几乎总是有更好的方法来组织这样的映射。)

如果您使用字典,那么如果您尝试使用尚未计算的值来索引您的字典,您将得到一个 KeyNotFoundException,并且不再有理由问“我该怎么办在默认情况下?”。

【讨论】:

  • +1,字典很好,但对于少量选项可能效率低下且不必要地详细说明。仍然很高兴知道对开关盒采取的正确措施。
【解决方案10】:

这些天我写了两个自定义异常:UnexpectedValueExceptionUnexpectedTypeException。在我看来,这些都是非常有价值的例外,因为一旦您发现发生了意外(即假定不存在)值或类型,您就应该抛出尽可能有意义的消息。

class UnexpectedValueException : Exception {
   public UnexpectedValueException(object pValue)
   : base($"The value '{pValue}' of type '{pValue?.GetType().ToString() ?? "<null>"}' was not expected to exist.") {
   }
}

enum SomeEnum {
   One,
   Two,
   Three
}

void someFunc() {
   SomeEnum value = someOtherFunc();

   switch (value) {
      case One: ... break;
      case Two: ... break;
      default: throw new UnexpectedValueException(value);    
   }
}

【讨论】:

  • 正确编写自定义异常类需要大量工作,并且需要考虑序列化等场景 (blogs.msdn.microsoft.com/agileer/2013/05/17/…)。我认为InvalidOperationException 非常适合这种使用枚举的特定情况。
  • @SedatKapanoglu 为什么自定义异常必须支持序列化?
  • @SedatKapanoglu 更不用说InvalidOperationException 在语义上是不正确的。 Invalid Operation 表示在对象的当前状态下不能调用给定的方法。因此,您以错误的顺序调用事物,或者尚未注入所需的依赖项,或者类似的东西。这里的例外是我们认为我们已经覆盖了所有值,但不知何故有一个值超出了我们假设存在的值范围,这表示该方法不完整,或者传递的值通常不应该存在.因此UnexpectedValueException.
【解决方案11】:

我决定使用的是ArgumentOutOfRangeException,带有一个自定义错误消息,灵感来自InvalidEnumArgumentException 的默认消息(我不喜欢使用它,因为它在 System.ComponentModel 中):

参数“{nameof(theArgument)}”({theArgument}) 的值对于枚举类型“{nameof(TheEnumType)}”无效。

这是它的样子:

return level switch
{
    LogEventLevel.Verbose => "VERBOSE",
    LogEventLevel.Debug => "DEBUG",
    LogEventLevel.Information => "INFO",
    LogEventLevel.Warning => "WARN",
    LogEventLevel.Error => "ERROR",
    LogEventLevel.Fatal => "FATAL",
    _ => throw new ArgumentOutOfRangeException(nameof(level), level,
        $"The value of argument '{nameof(level)}' ({level}) is invalid for enum type '{nameof(LogEventLevel)}'.")
};

【讨论】:

  • 但这不是争论?
  • @SedatKapanoglu 如果您创建一个将某个枚举值转换为另一个值的方法,那么它实际上是一个参数。否则,您可以将枚举值视为switch 语句的参数。
  • “如果你创建一个将某个枚举值转换为另一个值的方法,那么它实际上是一个参数”,不,不是真的。 “论据”有一个非常具体的定义。 stackoverflow.com/questions/156767/…
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-01-10
  • 1970-01-01
  • 2020-12-03
  • 2011-12-07
  • 1970-01-01
  • 2013-06-08
相关资源
最近更新 更多