处理枚举时没有默认的switch语句

41

自从我开始使用.NET以来,这一直是我的一个小烦恼,但我很好奇是否有什么东西我错过了。我的代码片段无法编译(请原谅示例的强制性质),因为(根据编译器)缺少返回语句:

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一无所知,并且很有可能会引入逻辑错误。

然而,如果编译器允许我使用上述代码,它就可以确定我们存在问题,因为我的情况语句将不再涵盖枚举中的所有值。对我来说,这听起来更加安全。

这对我来说非常基本错误,我想知道是否只是我错过了什么,还是我们在使用switch语句中的枚举时必须非常小心?

编辑: 我知道我可以在默认情况下引发异常或添加返回到switch之外,但这些仍然是根本上解决编译器错误的黑客方法。

关于枚举实际上只是int的一个.NET的肮脏的秘密,这真的很令人尴尬。请允许我声明一个具有有限可能性的枚举,并为我提供一个编译:

Decision fred = (Decision)123;

如果有人尝试这样做,就抛出一个异常:

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

编辑2:

一些人对于当枚举在不同程序集中时会发生什么以及这会导致问题提出了评论。我的观点是,这就是我认为应该发生的行为。如果我更改方法签名,这将导致问题,我的前提是更改枚举应该是相同的情况。我有印象很多人认为我不理解.NET中的枚举。实际上我理解,只是认为这种行为是错误的,并希望有人能了解一些非常模糊的功能,可以改变我的关于.NET枚举的看法。


10
我认为你的意思是 "public enum Decision { Yes, No, FileNotFound }"。 - Juliet
1
朱丽叶,你翻译得很好...但是你可能应该明确一下这是个笑话,我不确定每个人都能看出来 ;) - Thomas Levesque
2
@jasonh 基于枚举可以更改,方法也可以更改的工作,这是否意味着所有类都应该有一个默认方法以备 .NET 找不到所请求的方法签名时使用? - Mark
1
@Luke编译器告诉我缺少一个返回语句,因为由于.NET枚举的实现方式,它可以找到一个没有返回值的代码路径。这只是因为.NET的枚举不是真正的枚举。如果它们是真正的枚举,那么在程序中只有两种可能的路线。 - Mark
2
@Mark:这不是由于enum的.NET实现,而是switch语句的工作方式:如果您使用bool并覆盖truefalse路径但省略了default,或者如果您使用byte并覆盖所有256个可能的路径而没有default;或者使用short并覆盖所有65536个路径而没有default等等,您将获得相同的错误。 - LukeH
显示剩余3条评论
10个回答

45

情况比仅仅处理枚举更糟糕。我们甚至没有为布尔型做到这一点!

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"; 
    }
  }
}

产生了相同的错误。

即使您解决了枚举能够采用任何值的所有问题,您仍将面临此问题。语言的流分析规则根本不认为没有默认情况的开关是对所有可能的代码路径的“详尽”,即使您和我知道它们是这样的。

我非常想修复它,但坦率地说,我们有许多更高的优先事项要解决,所以我们从未去做过。


4
感谢Eric能够理解我的冗长表述,看到问题的本质。但是,你不觉得在枚举类型的情况下,强制添加一个多余的默认情况实际上可能会导致问题吗?这个默认情况可能会变成一个危险的情况。 - Mark
10
没错。枚举类型只不过是一种高级的整数,而且它们的定义可能会随版本变化而改变,这使得使用它们变得危险。这很不幸;如果你下次从零开始设计类型系统,就要知道不要重复这个错误。 - Eric Lippert
这个看起来现在已经修复了 :) - Wouter

25

default 子句中抛出异常:

default:
    throw new ArgumentOutOfRangeException("decision");

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


20

这是因为decision的值实际上可能是枚举中没有的值,例如:

string s = GetDecision((Decision)42);

编译器或CLR不能防止这种情况。该值也可能是枚举值的组合:

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

即使枚举没有Flags属性,你也应该在switch语句中加入default情况,因为无法显式检查所有可能的值。


我很欣赏你能做这些事情,但对我来说,这只是.NET让你自取灭亡的案例。 - Mark
实际上,对于带有“Flags”属性的枚举来说,这是有意义的……这个属性意味着您可以组合值,这通常会导致不显式属于该枚举但仍然有效的值。但是我希望编译器只允许在“Flags”枚举中执行此操作…… - Thomas Levesque
1
实际上,它为您提供了足够的自由度,以有效地与现有的COM和WIN32 API进行交互,这些API为其标志定义了枚举整数类型。但是,是的,您必须小心使用这个自由度。 - Eric Lippert

9
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.

        }
    }
}

3
默认设置是为了保护您。从默认设置中抛出异常,如果有人添加额外的枚举,您可以使用某些标志进行覆盖。

我知道我可以这么做,但它仍然会引发一个可以避免的运行时异常。.NET 强制你使用 break 而不允许 fall-throughs 来避免逻辑错误,强制使用 default 只会带来麻烦。 - Mark
1
由于Thomas L所说的原因,它无法在编译时被捕获,因此默认值存在是为了确保它被捕获。 - Joel Goodwin

2

我知道这是一个旧帖被重新激活的过程...

就个人而言,我觉得switch语句的工作方式是正确的,它按照我的逻辑运行。

我很惊讶听到有人对默认标签进行如此抱怨。

如果您只有一组严格的枚举或值需要测试,您不需要所有的异常处理行,或者在switch之外返回等等。

只需将默认标签放在其他标签上面,也许是最常见的响应标签之一。在您的示例中,可能无论选择哪个都没有关系。简洁明了,它满足了您消除编译器警告的需求:

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

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

1
我意识到这是一个线程复活 - 这种讽刺并没有逃过我的注意... OP 的观点是,如果你在枚举中添加一个 Decision.Maybe,那么你的建议就不起作用了。对于 Maybe,当前的代码路径都不适用,因此你的代码将隐藏一个 bug。如果允许省略默认值,编译器可以说:“等等!如果决策是 maybe,你没有返回任何东西!”在编译时,你将有潜在的 bug 被标记。我怀疑 OP 知道你可以将默认值放到现有分支上,只是他们不想这样做。 - Chris

2
为了分享一个古怪的想法,即使没有其他目的,也来试试:

您总是可以实现自己的强枚举

...而且自从引入nameof运算符以来,您也可以在switch-case语句中使用它们。 (虽然在技术上以前也可以这样做,但很难使这种代码易读和易于重构。)

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); // 返回2

现在我们可以轻松地获得名称,这基本上只是一个额外的奖励,有些读者可能会指出,这基本上是一个没有空值情况(因为它不是类)的Java枚举。就像在Java中一样,您可以向其添加任何额外的数据和/或属性,例如序数值。

可读性:检查!
智能感知:检查!
重构能力:检查!
是值类型:检查!
真正的枚举:检查!
...
它的性能如何?与本机枚举相比,不行。
你应该使用它吗?嗯......

对于您来说,拥有真正的枚举以便摆脱枚举运行时检查及其伴随的异常有多重要?
我不知道。无法真正回答您,亲爱的读者;每个人都有自己的喜好。

实际上,当我写这个时,我意识到让结构体“包装”一个普通的枚举可能会更清晰。 (静态结构字段和相应的普通枚举通过类似上面的反射相互映射。)只要不将普通枚举用作参数,你就没问题了。

更新:

是的,我花了一整晚测试我的想法,我是对的:现在我在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<T, U>并继承空的IEnumerationMarker接口。
  4. Enumeration<T, U>的第一个泛型类型参数必须是您的枚举类。
  5. 对于每个公共静态字段,必须存在一个在System.Enum(您指定为Enumeration<T, U>的第二个泛型类型参数)中具有相同名称的值。
  6. 所有公共静态字段都必须是只读的,并且是您的枚举类型。
  7. 在类型初始化期间,必须为所有公共静态字段分配唯一的非空值。
目前,在类型初始化时断言了上述每个不变量。稍后可能会尝试进行微调,以查看是否可以在编译时检测其中的一些内容。
要求的理由:
您的枚举必须是密封的,因为如果不是这样,其他不变量会变得更加复杂,而没有明显的好处。
允许公共构造函数没有意义。它是一个枚举类型,基本上是一个单例类型,但实例集合是固定的,而不仅仅是一个。
与第一个原因相同。如果不是这样,反射和一些其他不变量和约束检查将变得混乱。
我们需要这个泛型类型参数,以便使用类型系统来使用高效的编译/Jit时间绑定唯一存储我们的枚举数据。没有哈希表或其他缓慢的机制!理论上可以删除它,但我认为这不值得增加复杂性和性能成本。
这个应该很明显。我们需要这些常量来制作优雅的switch语句。当然,我可以制作一个没有它们的第二个枚举类型;你仍然可以使用之前显示的nameof方法进行切换。只是不那么高效。我还在考虑是否应该放宽这个要求。我会试验一下......
您的枚举常量必须是公共静态的,因为明显的原因是只读字段。拥有只读枚举实例意味着所有等式检查简化为引用相等;属性更灵活和冗长,而且这两个都不是枚举实现所需的属性。最后,所有公共静态字段必须是您的枚举类型,因为:它使您的枚举类型保持整洁,减少杂乱;让反射更简单;并且您可以自由地使用属性做任何事情,因此这是一个非常柔性的限制。
这是因为我正在努力将“恶劣的反射魔法”最小化。我不希望我的枚举实现需要完全信任执行。那将严重限制其有用性。更准确地说,在低信任环境中调用私有构造函数或写入只读字段可能会引发安全异常。因此,您的枚举必须在初始化时实例化您的枚举常量-然后我们可以“清洁地”填充这些实例的(内部)基类数据。
无论如何,你怎样使用这些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;
    }
}

抽象枚举类还将为我们实现IEquatable<Example>接口,包括可以在Example实例上使用的==和!=运算符。除了类型初始化期间需要反射之外,一切都很干净且高效。可能会继续实现Java中专门为枚举定义的集合。那么这段代码在哪里呢?在发布之前,我想看看能否进一步简化它,但最后可能会在GitHub的dev分支上放出——除非我找到其他疯狂的项目要工作!^_^

现在已经可以在GitHub上找到了
请查看Enumeration.csEnumeration_T2.cs
它们目前是我正在开发的非常wip库的dev分支的一部分。
(目前没有“可发布”内容,并且随时可能出现破坏性更改。)
... 目前该库的其余部分主要是大量样板文件,用于扩展所有数组方法以适用于多秩数组,使多秩数组可与Linq一起使用,并且进行性能优化的ReadOnlyArray包装器(不可变结构体),以安全的方式公开(私有)数组,而无需每次都创建副本。

除了最新dev提交之外的所有内容*都得到了充分的文档记录和IntelliSense友好。
(*java枚举类型仍然在开发中,一旦完成其设计,将进行适当的文档记录。)


有人仍然可以更新MyEnum,任何没有默认异常的开关都会导致逻辑错误。 - Wouter
@Wouter 当然,标准枚举也是如此。好的程序员永远不会更改枚举。聪明的程序员永远不会忽略默认情况。 - AnorZaken
1
真实的情况是,大多数开发人员和非开发人员都不关心枚举设计准则,因此请参阅以下链接中的“不要这样做”部分:https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/enum。现在尝试说服那些非开发人员...他们通常不关心,就像大多数开发人员一样...所以我个人会抛出异常,这样至少我知道有人没有遵守准则。 - Wouter
@Wouter 智能枚举绝对不是每个问题的解决方案,但使用智能枚举可以将一些责任封装到枚举类型本身中。例如,如果您使用枚举来决定如何格式化文本,则每个枚举值都可以包含一个包含格式字符串的属性,甚至可以具有接受字符串并进行格式化的方法。现在,如果您添加一个新的枚举值,它要么无法编译(将格式字符串放入枚举值构造函数中),要么很难忘记,因为格式化方法驻留在智能枚举类中。 - AnorZaken
澄清一下,我的观点是,你可以用枚举做的所有事情,智能枚举都可以更好地完成,或者至少是相当的,并且它还可以做常规枚举无法做到的事情。这是一个不需要任何证明的陈述,因为它仅仅是从智能枚举允许你回退到常规枚举值这一事实中得出的。因此,在最坏的情况下,如果你发现有一种情况更适合使用常规枚举值,你总是可以将智能枚举值作为常规枚举值来使用。我并不是说智能枚举是万能的解决方案,但在所有情况下,它们应该比常规枚举更好或者相等。 - AnorZaken
显示剩余4条评论

1

我总是认为默认情况下是一种“落空/异常”。

所以这里不应该是“可能”,而应该是“无效的决定,请联系支持部门”。

我不明白它为什么会落到那个地方,但那将成为万能的/异常情况。


0

不要抱怨 switch 语句的工作方式,而是通过在枚举上使用扩展方法(如 此处此处 所述)完全避免使用它。

这种方法的好处是,当添加新的枚举值时,你不会忘记更新 GetDecision switch 语句,因为它们都在同一个地方 - 在枚举声明中。

这种方法的效率对我来说是未知的,实际上,在这一点上甚至不需要考虑。没错,我只关心它对有多么容易,我想,“哼,计算机就是为了辛苦工作而存在的。”(当 Skynet 掌控一切时,我可能会后悔这种态度。)

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

通常我会将“未设置”作为第一个枚举元素,因为正如楼主所指出的那样,C# 枚举实际上是 int 类型,所以未初始化的变量将会是零或第一个枚举值

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();
}

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接