处理枚举时,切换语句没有默认值

时间:2009-07-08 15:04:15

标签: c# .net enums switch-statement

自从我开始使用.NET以来,这一直是我的一个宠儿,但我很好奇,以防我遗漏了一些东西。我的代码片段无法编译(请原谅样本的强制性质),因为(根据编译器)缺少return语句:

public enum Decision { Yes, No}

    public class Test
    {
        public string GetDecision(Decision decision)
        {
            switch (decision)
            {
                case Decision.Yes:
                    return "Yes, that's my decision";
                case Decision.No:
                    return "No, that's my decision";

            }
        }
    }

现在我知道我可以简单地放置一个默认语句来摆脱编译器警告,但在我看来,不仅是冗余代码,它的危险代码。如果枚举在另一个文件中并且另一个开发人员出现并将 Maybe 添加到我的枚举中,它将由我的默认子句处理,该子句对 Maybe s一无所知并且确实存在我们很有可能引入逻辑错误。

然而,如果编译器允许我使用上面的代码,那么它可以识别我们有问题,因为我的case语句将不再覆盖枚举中的所有值。当然听起来对我来说更安全。

这对我来说根本就是错误的,我想知道它是否只是我缺少的东西,还是我们在switch语句中使用枚举时必须非常小心?

修改 我知道我可以在默认情况下引发异常或在交换机外添加一个返回,但这仍然是根本不能解决编译错误,这不应该是错误。

关于enum真的只是一个int,这是.NET肮脏的小秘密之一,真的非常令人尴尬。让我声明一个有限数量的可能性的枚举,并给我一个汇编:

Decision fred = (Decision)123;

然后如果有人尝试这样的话会抛出异常:

int foo = 123;
Decision fred = (Decision)foo;

编辑2:

有些人对enum在不同的程序集中会发生什么以及如何导致问题发表评论。我的观点是,这是我认为应该发生的行为。如果我更改方法签名,这将导致问题,我的前提是更改枚举应该是相同的。我的印象是很多人都不认为我理解.NET中的枚举。我只是认为行为是错误的,我希望有人可能知道一些非常模糊的功能会改变我对.NET枚举的看法。

10 个答案:

答案 0 :(得分:37)

哎呀,这种情况远比处理枚举更糟糕。我们甚至不为布尔做这个!

public class Test {        
  public string GetDecision(bool decision) {
    switch (decision) {
       case true: return "Yes, that's my decision";                
       case false: return "No, that's my decision"; 
    }
  }
}

产生同样的错误。

即使您解决了枚举能够承担任何价值的所有问题,您仍然会遇到此问题。语言的流分析规则根本不考虑没有默认值的开关是“穷举”所有可能的代码路径,即使你和我知道它们都是。

我非常想解决这个问题,但坦率地说,我们有很多优先事项而不是解决这个愚蠢的小问题,所以我们从未接触过它。

答案 1 :(得分:17)

那是因为decision的值实际上可能是不属于枚举的值,例如:

string s = GetDecision((Decision)42);

编译器或CLR不会阻止此类事情。该值也可以是枚举值的组合:

string s = GetDecision(Decision.Yes | Decision.No);

(即使枚举没有Flags属性)

正因为如此,您应该始终在切换中放置default个案,因为您无法明确检查所有可能的值

答案 2 :(得分:15)

默认子句中抛出异常:

default:
    throw new ArgumentOutOfRangeException("decision");

这可确保涵盖所有可能的路径,同时避免因添加新值而导致的逻辑错误。

答案 3 :(得分:7)

public enum Decision { Yes, No}

public class Test
{
    public string GetDecision(Decision decision)
    {
        switch (decision)
        {
            case Decision.Yes:
                return "Yes, that's my decision";
            case Decision.No:
                return "No, that's my decision";
            default: throw new Exception(); // raise exception here.

        }
    }
}

答案 4 :(得分:2)

默认设置是为了保护您。从默认值中抛出一个异常,如果有人添加额外的枚举,你就会被标记出来。

答案 5 :(得分:2)

我意识到这是一个线程复活......

我个人认为交换机的工作方式是正确的,它按照逻辑上的要求运行。

我很惊讶听到这样的抱怨默认标签。

如果您只测试一组严格的枚举或值,则不需要所有异常处理行,也不需要在交换机外部返回等。

只需将默认标签放在其他标签之一上,也许是最常见响应的标签。在您的示例中,哪个可能无关紧要。简短,甜蜜,它满足了你摆脱编译器警告的需求:

switch (decision)
{
    default:
    case Decision.Yes:
        return "Yes, that's my decision";
    case Decision.No:
        return "No, that's my decision";
}

如果您不希望默认值为“是”,请将默认标签放在“无标签”上方。

答案 6 :(得分:2)

为了分享一个奇怪的想法,如果没有别的,这里是:

您始终可以实施自己的强力词汇

...自从引入nameof运算符以来,您也可以在switch-cases中使用它们。 (并不是说你以前没有技术上这么做,但很难让这些代码可读并且重构友好。)

public struct MyEnum : IEquatable<MyEnum>
{
    private readonly string name;
    private MyEnum(string name) { name = name; }

    public string Name
    {
        // ensure observable pureness and true valuetype behavior of our enum
        get { return name ?? nameof(Bork); } // <- by choosing a default here.
    }

    // our enum values:
    public static readonly MyEnum Bork;
    public static readonly MyEnum Foo;
    public static readonly MyEnum Bar;
    public static readonly MyEnum Bas;

    // automatic initialization:
    static MyEnum()
    {
        FieldInfo[] values = typeof(MyEnum).GetFields(BindingFlags.Static | BindingFlags.Public);
        foreach (var value in values)
            value.SetValue(null, new MyEnum(value.Name));
    }

    /* don't forget these: */
    public override bool Equals(object obj)
    {
        return obj is MyEnum && Equals((MyEnum)obj);
    }
    public override int GetHashCode()
    {
        return Name.GetHashCode();
    }
    public override string ToString()
    {
        return Name.ToString();
    }
    public bool Equals(MyEnum other)
    {
        return Name.Equals(other.Name);
    }
    public static bool operator ==(MyEnum left, MyEnum right)
    {
        return left.Equals(right);
    }
    public static bool operator !=(MyEnum left, MyEnum right)
    {
        return !left.Equals(right);
    }
}

并因此使用它:

public int Example(MyEnum value)
{
    switch(value.Name)
    {
        default: //case nameof(MyEnum.Bork):
            return 0;
        case nameof(MyEnum.Foo):
            return 1;
        case nameof(MyEnum.Bar):
            return 2;
        case nameof(MyEnum.Bas):
            return 3;
    }
}

你当然会这样称呼这种方法:
int test = Example(MyEnum.Bar); // returns 2

我们现在可以轻松获取名称基本上只是一个奖励,是的,有些读者可能会指出这基本上是一个没有空案例的 Java 枚举(因为它是&#39; s不是一个班级)。就像在Java中一样,您可以添加任何您想要的额外数据和/或属性,例如一个序数值。

可读性:检查!
智能感知:检查!
可重构性:检查!
是ValueType: Check!
真实枚举:检查!
...
它表现得好吗? 与本地枚举相比;号
你应该用这个吗? 嗯....

拥有真正的枚举对你有多重要,这样你就可以摆脱枚举运行时检查及其伴随的异常?
我不知道。亲爱的读者,真的无法回答这个问题。他们各自的。

......实际上,正如我写的那样,我意识到让结构&#34;包裹&#34;正常的枚举。 (静态结构字段和相应的正常枚举在上面的类似反射的帮助下互相镜像。)永远不要使用普通的枚举作为参数,你就是好的。

更新:

Yepp,过夜测试我的想法,我说得对:我现在在c#中有近乎完美的java风格的枚举。使用清洁,性能得到改善。 最重要的是:所有讨厌的狗屎都封装在基类中,你自己的具体实现可以像这样干净:

// example java-style enum:
public sealed class Example : Enumeration<Example, Example.Const>, IEnumerationMarker
{
    private Example () {}

    /// <summary> Declare your enum constants here - and document them. </summary>
    public static readonly Example Foo = new Example ();
    public static readonly Example Bar = new Example ();
    public static readonly Example Bas = new Example ();

    // mirror your declaration here:
    public enum Const
    {
        Foo,
        Bar,
        Bas,
    }
}

这是你可以做的事情:

  • 您可以添加所需的任何私人字段。
  • 您可以添加所需的任何公共非静态字段。
  • 您可以添加所需的任何属性和方法。
  • 您可以根据需要设计构造函数,因为:
  • 你可以忘记基础构造函数的麻烦。基础构造函数是无参数的!

这是必须做的事情:

  1. 你的枚举必须是密封的。
  2. 所有构造函数都必须是私有的。
  3. 您的枚举必须直接从Enumeration&lt; T,U&gt;继承。并继承空的IEnumerationMarker接口。
  4. Enumeration&lt; T,U&gt;的第一个泛型类型参数。必须是你的enum课程。
  5. 对于每个公共静态字段,System.Enum中必须存在具有相同名称的值(您指定为Enumeration&lt; T,U&gt;的第二个泛型类型参数)。
  6. 所有公共静态字段必须是只读的和枚举类型。
  7. 在类型初始化期间,必须为所有公共静态字段分配唯一的非空值。
  8. 此时,在类型初始化时断言上面的每个不变量。可能会尝试稍后调整它以查看是否可以在编译时检测到其中一些。

    要求理由:

    1. 你的枚举必须是密封的,因为如果它不是,那么其他不变量会变得更复杂,没有明显的好处。
    2. 允许公共构造函数毫无意义。它是一个枚举类型,基本上是一个单例类型,但有一组固定的实例,而不只是一个。
    3. 与第一个相同的原因。如果它不是,反射和其他一些不变量和约束检查就会变得混乱。
    4. 我们需要这个泛型类型参数,因此类型系统可以用于使用高性能的编译/ Jit时间绑定来唯一地存储我们的枚举数据。没有使用散列表或其他慢速机制!从理论上讲,它可以删除,但我不认为这样做会增加复杂性和性能的成本。
    5. 这个应该很明显。我们需要这些常量来制作优雅的switch语句。当然,我可以制作一个没有它们的第二个枚举类型;您仍然可以使用前面显示的nameof方法进行切换。它不会那么高效。我还在考虑是否应该放松这个要求。我会对它进行实验......
    6. 由于显而易见的原因,你的枚举常量必须是公共的和静态的,而readonly字段因为a)具有只读枚举实例意味着所有相等检查都简化为引用相等; b)属性更灵活,更冗长,坦率地说,这两者都是枚举实现的不良属性。最后,所有公共静态字段必须是你的枚举类型,因为; a)它使您的枚举类型更清洁,减少混乱; b)使反射更简单; c)无论如何你都可以自由地做任何属性,所以这是一个非常软的限制。
    7. 这是因为我努力保持&#34;讨厌的反思魔法&#34;至少。我不希望我的枚举实现需要完全信任执行。这将严重限制其有用性。更确切地说,调用私有构造函数或写入只读字段可以在低信任环境中抛出安全异常。因此,你的枚举必须在初始化时自己实例化你的枚举常量 - 然后我们可以填充那些实例的(内部)基类数据&#34;干净地&#34;。
    8. 所以,无论如何,你如何使用这些java风格的枚举?

      我现在实现了这个东西:

      int ordinal = Example.Bar.Ordinal; // will be in range: [0, Count-1]
      string name = Example.Bas.Name; // "Bas"
      int count = Enumeration.Count<Example>(); // 3
      var value = Example.Foo.Value; // <-- Example.Const.Foo
      
      Example[] values;
      Enumeration.Values(out values);
      
      foreach (var value in Enumeration.Values<Example>())
          Console.WriteLine(value); // "Foo", "Bar", "Bas"
      
      public int Switching(Example value)
      {
          if (value == null)
              return -1;
      
          // since we are switching on a System.Enum tabbing to autocomplete all cases works!
          switch (value.Value)
          {
              case Example.Const.Foo:
                  return 12345;
              case Example.Const.Bar:
                  return value.GetHasCode();
              case Example.Const.Bas:
                  return value.Ordinal * 42;
              default:
                  return 0;
          }
      }
      

      抽象Enumeration类还将为我们实现IEquatable<Example>接口,包括可用于Example实例的==和!=运算符。

      除了在类型初始化期间所需的反射外,一切都很干净且高效。可能会继续实现java对枚举的专门集合。

      那么这段代码在哪里?

      我想看看我是否可以在发布之前将其清理一下,但是在本周末它可能会在GitHub的dev分支上启动 - 除非我发现其他疯狂的项目要工作! ^ _ ^

      现在开始GitHub
      请参阅Enumeration.csEnumeration_T2.cs 他们目前是我工作的一个非常狡猾的图书馆的开发分支的一部分。
      (没有什么是&#34;可释放&#34;但是随时都会受到重大变化。)
      ...目前,库的其余部分主要是大量的样板文件,用于将所有数组方法扩展到多级数组,使多级数组可用于Linq,以及高性能的ReadOnlyArray包装器(不可变结构)用于公开(私有)安全的数组,没有cringy需要一直创建副本。

      除了最新的dev提交之外的所有内容都完全记录并且IntelliSense友好 (* java enum类型仍然是wip,一旦我完成了他们的设计,就会被正确记录。)

答案 7 :(得分:1)

我总是将默认视为坠落/异常。

所以在这里它可能不是,而是“无效的决定,联系支持”。

我不知道它会如何落到那,但这将是一个笼罩/异常案例。

答案 8 :(得分:0)

除了这种情况之外,你可以将任何int强制转换为你的枚举并拥有一个你没有处理的枚举。还有一种情况是,如果枚举位于外部.dll中,并且.dll已更新,则如果在枚举中添加了其他选项(例如,是,否,可能),则不会破坏您的代码。因此,要处理这些未来的更改,您还需要默认情况。在编译时无法保证你知道枚举对其生命的每一个价值。

答案 9 :(得分:0)

与其抱怨switch语句的工作原理,不如通过herehere所述,对枚举使用扩展方法来完全避免它。

这种方法的好处是,您不会陷入困境,而在添加新的枚举值时忘记了更新GetDecision开关语句,因为它们都在同一位置-枚举声明中。

我不知道这种方法的效率,实际上,现在甚至还没有考虑。没错,我不在乎,因为对我来说要容易得多,所以我想:“应该拧紧那台计算机的用途-努力工作。” (当天网接任时,我可能会后悔这种态度。)

如果我需要从这些属性值之一返回到枚举值,我可以简单地建立一个反向字典并用单行代码填充它。

(我通常将“未设置”作为第一个枚举元素,因为正如OP所述,C#枚举实际上是一个int,所以an uninitialized variable is going to be zero or the first enum value

public enum Decision
{
  [DisplayText("Not Set")]
  NotSet,
  [DisplayText("Yes, that's my decision")]
  Yes,
  [DisplayText("No, that's my decision")]
  No
}

public static class XtensionMethods
{
  public static string ToDisplayText(this Enum Value)
  {
    try
    {
      Type type = Value.GetType();
      MemberInfo[] memInfo =
        type.GetMember(Value.ToString());

      if (memInfo != null && memInfo.Length > 0)
      {
        object[] attrs = memInfo[0]
          .GetCustomAttributes(typeof(DisplayText), false);
        if (attrs != null && attrs.Length > 0)
          return ((DisplayText)attrs[0]).DisplayedText;
      }
    }
    catch (Exception ex)
    {
      throw new Exception(
        "Error in XtensionMethods.ToDisplayText(Enum):\r\n" + ex.Message);
    }
    return Value.ToString();
  }

  [System.AttributeUsage(System.AttributeTargets.Field)]
  public class DisplayText : System.Attribute
  {
    public string DisplayedText;

    public DisplayText(string displayText)
    {
      DisplayedText = displayText;
    }
  }
}

使用内联,如:

myEnum.ToDisplayText();

或包装在函数中,如果您愿意的话:

public string GetDecision(Decision decision)
{
  return decision.ToDisplayText();
}