何时使用枚举,何时用具有静态成员的类替换它们?

35

最近我意识到以下(示例)枚举...

enum Color
{
    Red,
    Green,
    Yellow,
    Blue
}

可以用一个看似更具类型安全性的类来替换...

class Color
{
    private Color() { }

    public static readonly Color   Red      = new Color();
    public static readonly Color   Green    = new Color();
    public static readonly Color   Yellow   = new Color();
    public static readonly Color   Blue     = new Color();
}

所谓“类型安全”,是指如果Color是枚举类型,则以下语句有效,但如果Color是上述类,则无效:

var nonsenseColor = (Color)17;    // works if Color is an enum

两个问题:

1) 这种模式(用类型安全的类替换枚举)是否有被广泛接受的名称?

2) 在哪些情况下应该使用枚举,而在何时使用类更为适合?


2
我不确定是否有一种命名模式可以用类替换枚举,但就个人而言,我并没有看到额外的价值。枚举对于像上面这样的情况来说是简单而高效的。 - KP.
2
Java中的枚举与您试图实现的内容类似。 - empi
1
枚举作为类的方法的一个优点是,你不能分配超出范围的值,正如我所演示的那样。除此之外,我不确定是否还有其他优点或者甚至缺点——这就是我想通过这个问题找出来的。 - stakx - no longer contributing
2
枚举类型非常便宜,它们是值类型。 - Hans Passant
1
@stakx:你也可以使用结构体而不是类来实现相同的“类型安全”,并实现在栈上保留一个小型/廉价数据对象的目标。在我看来,枚举是最安全的选择。你试图防止的声明只是一种糟糕的编程习惯。 - Joel Etherton
1
@stakx:我只是有点无情吧。如果有人滥用我的枚举,我会觉得他应该承受相应的痛苦 >:) - Joel Etherton
6个回答

31

枚举类型非常适合轻量级的状态信息。例如,您的颜色枚举(不包括蓝色)可以用于查询交通灯的状态。真正的颜色以及颜色的整个概念和所有相关信息(alpha、颜色空间等)并不重要,只关心灯的状态。此外,稍微更改枚举来表示交通灯的状态:

[Flags()]
public enum LightColors
{
    unknown = 0,
    red = 1,
    yellow = 2,
    green = 4,
    green_arrow = 8
}

当前的灯光状态可以设置为:

LightColors c = LightColors.red | LightColors.green_arrow;

并查询为:

if ((c & LightColors.red) == LightColors.red)
{
    //Don't drive
}
else if ((c & LightColors.green_arrow) == LightColors.green_arrow)
{
    //Turn
}

静态类颜色成员可以支持多种状态而不需要额外的功能。

然而,静态类成员非常适合常用对象。例如,System.Drawing.Color 成员是很好的例子,因为它们表示一些已知名称的颜色,但其构造函数却比较晦涩(除非你知道十六进制颜色代码)。如果使用枚举来实现这些颜色,每次想要使用值作为颜色时都需要像这样做:

colors c = colors.red;
switch (c)
{
    case colors.red:
        return System.Drawing.Color.FromArgb(255, 0, 0);
        break;
    case colors.green:
        return System.Drawing.Color.FromArgb(0,255,0);
        break;
}

所以,如果你有一个枚举类型,并且发现你不断地使用 switch/case/if/else 等语句来派生出一个对象,那么你可能想要使用静态类成员。如果你只是查询某个东西的状态,我建议还是使用枚举类型。此外,如果你需要以不安全的方式传递数据,枚举类型可能比你的对象的序列化版本更能存活。
编辑: @stakx,我认为你在回复 @Anton 的帖子时也碰到了一些重要的问题,那就是复杂性或者说,它对谁来说是复杂的?
从消费者的角度来看,我会非常喜欢 System.Drawing.Color 静态类成员,而不必编写所有这些内容。然而,从生产者的角度来看,编写所有这些内容可能会很麻烦。因此,如果其他人将使用您的代码,即使编写/测试/调试可能需要花费您10倍的时间,使用静态类成员可能会为他们节省很多麻烦。但是,如果只有你自己使用代码,你可能会发现使用枚举类型并根据需要进行强制转换更容易。

很好的回答,Chris。我接受了你的答案,因为我觉得它很好地综合了几个重要的观点。 - stakx - no longer contributing
2
枚举很棒,我们应该让枚举再次伟大。 - Andreas
上面的"[Flags()]"是什么意思,它是用来做什么的? - dsanchez
1
@dsanchez [Flags()] 是一个属性,它表示内部的值可以被视为一组位标志。这个来自MSDN文档的例子演示了如何使用[Flags()]属性通过使用按位OR返回枚举的多个值。 - The Fluffy Robot
这里有一个关于标志使用的不错解释:https://dev59.com/i3VD5IYBdhLWcg3wWKPc#8480 - carloswm85

3

如果有其他人感兴趣,我在此期间发现了一些事情:

  • switch块无法与枚举作为类一起使用。

  • 用户 empi 提到了上述枚举作为类示例与Java枚举的相似之处。在Java世界中,似乎有一种被称为类型安全枚举的公认模式; 显然,这种模式可以追溯到Joshua Bloch的书籍Effective Java


1
在我看来,switch 是一种反模式,因为当添加枚举值时会创建多个接触点。switch 中的代码应该是类中的一个方法。 - Doug Domeny
switch块无法与枚举作为类一起使用,实际上,那时它是一个优点,阻止我们使用反模式... 现在我们可以在Switch表达式中进行模式匹配,好多了。 - Marcelo Scofano Diniz

3

我在工作中遇到了类似的问题。

它基于John B发布的示例和Jon Skeet的博客文章“C#中的增强枚举”中的链接

如果你查看这篇博客中发布的基础知识,你会注意到派生类将共享一组常量值,这会导致严重的类型问题。当某个枚举基类在两个不同的其他类中派生时,它们的值集合也将在同一个集合中。

我的意思是,你很难创建一个DerivedEnumClass1变量,并在其中存储一个实际上属于BaseEnumClass1类型的枚举集合中的值,而不需要解决问题。

另一个问题是我们经常使用的Xml序列化。我使用了两个通用类作为业务对象上枚举变量的数据类型:一个表示单个枚举值,一个表示一组枚举值,我用它来模拟标志枚举行为。它们处理了存储在它们所代表的属性中的实际值的xml序列化和反序列化。一些运算符重载就足以这样重新创建位标志行为。
这些是我遇到的一些主要问题。
总的来说,我对最终结果并不满意,其中有一些不好的东西,但现在它能够胜任工作。
为什么我们的首席架构师决定仍然尝试让它工作,是因为可以使用实际的类并通过所有种类的行为进行扩展,无论是通用还是特定的。到目前为止,我还没有看到很多无法通过扩展方法提供的东西,但我们可能会遇到某些问题。
这里没有任何代码,而且我周末也无法处理它,否则我可以展示我已经做了哪些工作。虽然不是很漂亮... :o

你分享的博客文章很有意思!然而,我有一个小顾虑就是使用枚举作为类的方法可能会使代码变得更加复杂,因为简单的枚举代码看起来更好。把一个枚举实现成一个类会让代码变得更加复杂,这个点很快就会被到达,我的观点是它变得复杂了。 - stakx - no longer contributing
实际枚举本身的代码并不复杂。您可以使用通用类将所有扩展枚举所需的基本功能放入其中。然后,这些枚举最终只需要静态字段和私有构造函数即可。再加上任何特定于该枚举的功能。我花了大部分时间让开发人员像使用常规枚举/位标志一样使用它们。但是,是的,您最好有非常充分的理由开始使用这种类型的枚举。 - Anton

1

当我需要与一些想要一些难以理解的位标志值(可能已经或)进行交互的旧代码进行交互时,我会使用类常量。在这种情况下,枚举可能看起来像

public enum Legacy : ushort
{
  SomeFlag1 = 0x0001,
  SomeFlag2 = 0x0002,
  // etc...
}

因为需要将枚举转换为适当的值,所以P/Invoke中的编组较难阅读。

如果它是一个const类变量,则不需要进行转换。


1

两种方法都是有效的。你应该根据情况选择。

我可以补充一下,枚举支持位运算作为标志(甚至有[Flags]属性来支持这种语义,并从枚举中生成漂亮的字符串)。

有一个非常类似的重构称为使用策略模式替换枚举。嗯,在你的情况下它并不完全适用,因为你的颜色是被动的,不像一个策略那样起作用。然而,为什么不把它看作是一个特殊情况呢?


0

没错,Joshua Bloch 的《Effective Java》原版在 Java 1.5 发布之前就已经发布,它演示了类型安全枚举模式。不幸的是,该书的最新版本面向的是 Java > 1.5,因此它使用 Java 枚举。

其次,在 Java 中不能从 int 强制转换为枚举。

Color nonsenseColor = (Color)17; // compile-error

不过我不能代表其他语言发表意见。


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