【问题标题】:.NET: Why aren't Enum's Range/Value Checked?.NET:为什么不检查枚举的范围/值?
【发布时间】:2009-01-11 13:38:37
【问题描述】:

这一直困扰着我。也许对 .NET 内部有一些核心知识的人可以向我解释一下。

假设我定义一个枚举如下:

public enum Foo
{
   Eenie = 1,
   Meenie = 2,
   Miney = 3,
   Moe = 4
}

现在,还假设在我的代码中的某处,我有以下代码:

int bar = (Foo)5;

这将编译得很好,并且不会引发任何异常,即使值 5 显然不是Foo 中定义的有效值。

或者,考虑以下几点:

public void ProcessFoo(Foo theFoo)
{
    // Do processing
}

public static void Main()
{
    ProcessFoo((Foo)5);
}

同样,也不例外。

在我看来,这应该会导致类型不匹配异常,因为 5 不是Foo。但设计师选择不这样做。

现在,我编写了一个扩展方法,可以验证是否是这种情况,调用它来确保是这种情况并没有什么大不了的,但我必须使用反射来做到这一点(以及它的所有性能处罚之类的)。

同样,有什么令人信服的理由可能促使决定不检查枚举?

供参考,来自the MSDN documentation for the Enum class

当你定义一个方法或属性时 将枚举常量作为 值,请考虑验证该值。 原因是你可以施放一个 枚举类型的数值 即使那个数值不是 在枚举中定义。

【问题讨论】:

  • 我不想在这里发动一场圣战,但这就是我更喜欢Java枚举的原因:在Java中,枚举类型是特殊的类,枚举值是对象而不是具有特殊含义的整数.这意味着您甚至不能将无效值传递给需要枚举(null 除外)的方法

标签: .net enums


【解决方案1】:

问题在于性能。对普通枚举(例如 Color)进行检查枚举非常简单

enum Color {
  Red,
  Blue
}

但问题在于用作位标志的枚举。

enum Property {
  IsFirst = 0x1,
  IsDefault = 0x2,
  IsLastAccessed = 0x4
}

必须对转换为 Enum 值的每个整数进行按位检查被认为过于昂贵。因此可以轻松转换为枚举值。

【讨论】:

  • 那么为什么不直接创建一个修饰符或不同类型的枚举呢?像严格的枚举、CheckedEnum 或类似的东西?对于 99% 的用户来说,性能不是问题……但允许使用虚假值的枚举是一个问题。
  • @Beepbeep:正如大多数问题归结为“X 是可能的,为什么不做 X?”,答案是“因为没有人被它打扰到真正这样做”。
  • 验证 [Flags] 枚举实际上会更快,因为您可以“或”所有值来获取有效掩码,然后根据该掩码检查值。对于非标志枚举,您必须检查所有定义的值。
【解决方案2】:

范围检查可能会产生不必要的成本。因此,不隐式执行它是合理的。如前所述,[Flags] 要求不进行此类检查。如果运行时会检查[Flags] 的存在,那么每次执行转换时仍会导致运行时损失。

解决这个问题的唯一方法是让编译器知道[Flags] 属性。我想这样做并不是为了减少硬编码到编译器中的运行时知识量。

【讨论】:

【解决方案3】:

如果它们被范围检查,你将如何拥有[Flags] 枚举并使用按位或组合它们?

一个例子是ControlStylesenum

【讨论】:

  • 人们会认为 [Flags] 属性的存在会抑制这种行为,因为范围检查是一种运行时行为。
  • 不,当然,我绝对不是这个意思。我认为这可能有助于解释我的观点。
【解决方案4】:

我想到了两个原因。首先,生成超出范围的值需要强制转换。如果你故意施放,为什么你会期望在运行时被打耳光?禁止演员表会容易得多。

另一个引人注目的是这个:

enum VeryHardToRangeCheck {
  one = 1,
  three = 3,
  five = 5
}

【讨论】:

    【解决方案5】:

    我可以看出两个原因:

    1. [标志] 在没有检查的情况下工作更顺畅。以您为例,

      (Foo)5 == Foo.Eenie | Foo.Moe;

    2. 枚举是一种值类型。如果你不初始化它,它将等于零。如果您想检查枚举值,则不清楚在这种情况下何时应引发异常 - 例如,当您创建包含此枚举作为字段的类的实例时,零值可能会潜入您的代码。

      所以当前的行为更加一致 - 你只知道你可以有超出范围的值并检查它们。

    此外,您应该始终明确地进行检查并为您无法处理的值抛出异常。否则,向枚举添加新值可能会改变现有代码的行为。幸运的是,您使用单个 switch 语句是一个返回值的方法,如果没有找到匹配项,编译器会让您明确指定要执行的操作 - 在它之后的 switch 的默认部分中,您将不得不返回一个值或抛出异常. NotSupportedExcpetion 在大多数情况下首选。

    【讨论】:

      【解决方案6】:

      这是因为(Foo)5Foo.Eenie | Foo.Moe

      【讨论】:

      • 只有在使用 [Flags] 时才会出现这种情况。现在你也可以说 (Foo)5 是 Foo.Meenie | Foo.Miney.
      • 是的,它也可以是 Foo.Meenie | Foo.Miney,但这不是编译器的问题,也许你的意思是让它这样工作,Mike Hofer 只问为什么这不是编译错误。当然,您可以使用 [Flags] 或使用其他 2 个 exp 值。
      • 是的,但是,在允许 Enum 和 Flags 为同一类型时,C# 延续了 C 的设计错误。
      【解决方案7】:

      我会说原因是因为枚举仅在编译时由编译器进行类型检查和内联。对于使用 Flags 属性扩展枚举特别有用(因为它突然变得向前兼容......

      【讨论】:

        【解决方案8】:

        Microsoft 的 C# Programming Guide 明确表示不要做你所要求的:

        可以为 meetingDay 分配任意整数值。例如,这行代码不会产生错误: meetingDay = (Days) 42。但是,您不应该这样做,因为隐含的期望是枚举变量将仅保存枚举定义的值之一。为枚举类型的变量分配任意值会带来很高的错误风险。

        int 实际上只是存储类型。其实it's possible to specify other integer storage types,就像字节:

        enum Days : byte {Sat=1, Sun, Mon, Tue, Wed, Thu, Fri};
        

        存储类型决定一个枚举使用多少内存。

        【讨论】:

          【解决方案9】:

          枚举检查范围和值。您只是使用 (Foo)5 语法明确规避了检查。

          C# 是一种静态类型语言,这意味着在编译时 验证所有类型。编译器证明程序中不存在类型错误,因此没有必要检查类型是否运行时——它们保证是正确的。

          运行时检查类型对于值类型是不可能的,因为值类型在运行时不携带类型信息。例如,无法在运行时判断一个整数是有符号还是无符号。同样对于枚举,它们在运行时只是整数,不携带任何关于它们是什么枚举的信息。类型安全纯粹发生在编译时。

          对于引用类型,它是不同的,因为它们确实在运行时携带类型信息,这就是为什么可以在运行时使用 GetType() 访问类型的原因。但值类型并非如此。 (与引用类型相比,这是使值类型小而快的部分原因。)

          但是当您使用(Foo)bar 类型转换表达式时会出现问题。对于您比编译器更了解的情况,类型转换基本上是类型系统中的一个逃生口。你告诉编译器你知道 bar 是一个有效的 Foo,编译器应该信任你。

          类型转换是 C# 中最令人困惑的部分之一,因为它们对于值类型和引用类型的工作方式完全不同。对于引用类型,它们执行运行时检查该值是否确实具有指定的类型。如果不是,则抛出异常。

          但是对于值类型,不可能进行这样的检查。相反,强制转换只是将值视为预期类型,并让 负责验证这是否是有效的操作。未经验证的强制转换会导致各种意想不到的结果。拿这个代码:

          var a = -1;
          var b = (uint)a;
          

          b 的值是多少?如果你猜对了4294967295,那么你猜对了。

          因此,归根结底,在转换值类型时,有责任确保该值对该类型有效。

          在几乎所有实际情况下,您都应该验证输入,例如通过使用 Enum.IsDefined。仅当您绝对确定该值有效时才应使用原始转换。但即便如此,请注意枚举可能是错误的。例如。考虑这段代码:

              enum Color {
                  Red,
                  Blue
              }
              enum Hat {
                  Cab,
                  TopHat
              }
              public static void Main(){
                  var a = Color.Blue;
                  var b = (int)a;
                  if (!Enum.IsDefined<Hat>(b)) throw new Exception();
                  var c = (Hat)b;
                  Console.Write(c); // writes TopHat
              }
          

          验证只能检查valid对于给定的枚举是否有效,但有关该值最初属于哪个枚举的信息会丢失。因此,这与始终保留其类型信息且从不在运行时更改类型的引用类型明显不同。

          【讨论】:

            【解决方案10】:

            对不起,死灵。您是否可能在这里将 Enum 与键值集合混淆了?

            当您将整数 Meenie=2 分配给枚举项时,您所做的只是说 Meenie 是第三个索引,如果未指定,它之后的所有内容都将采用 2+(与 Meenie 的距离)。因此,当您查找 Foo[5] 时,您正在查找索引 5,而不是某个以 5 作为值的键。

            在大多数情况下,无论如何您都不会这样做;你会要求 Foo.Meenie - 这是枚举的 point,设置一个 known 值范围,然后通过它们的公开名称引用它们。这只是开发人员的便利。有更好的结构来做你在你的例子中所做的事情。

            【讨论】:

            • 我不知道在这种情况下枚举与键值对有何关联。枚举就是枚举:一组强类型常量。键值对完全不同(想想哈希表或字典),通常设计用于快速搜索。我在原帖中的观点是,可以通过某种方式对枚举进行范围检查,但其他人指出为什么不是这样。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多