枚举会创建脆弱的接口吗?


17

考虑下面的示例。对ColorChoice枚举的任何更改都会影响所有IWindowColor子类。

枚举会导致界面脆弱吗?是否有比枚举更好的东西可以提供更多的多态灵活性?

enum class ColorChoice
{
    Blue = 0,
    Red = 1
};

class IWindowColor
{
public:
    ColorChoice getColor() const=0;
    void setColor( const ColorChoice value )=0;
};

编辑:很抱歉使用颜色作为我的示例,这不是问题所在。这是一个不同的示例,它避免出现红色鲱鱼,并提供有关灵活性的更多信息。

enum class CharacterType
{
    orc = 0,
    elf = 1
};

class ISomethingThatNeedsToKnowWhatTypeOfCharacter
{
public:
    CharacterType getCharacterType() const;
    void setCharacterType( const CharacterType value );
};

进一步,假设通过工厂设计模式分发了适当的ISomethingThatNeedsToKnowWhatTypeOfCharacter子类的句柄。现在,我有了一个将来无法扩展为允许的字符类型为{human,dwarf}的其他应用程序的API。

编辑:只是为了更具体地说明我在做什么。我正在设计此(MusicXML)规范的强绑定,并且使用枚举类来表示规范中使用xs:enumeration声明的那些类型。我正在考虑下一个版本(4.0)出现时会发生什么。我的类库可以在3.0模式和4.0模式下工作吗?如果下一个版本是100%向后兼容的,那么也许是。但是,如果从规范中删除了枚举值,那么我就死定了。


2
当您说“多态灵活性”时,您确切地想到什么功能?
Ixrec


3
颜色使用枚举会创建脆弱的界面,而不仅仅是“使用枚举”。
布朗

3
添加新的枚举变量会破坏使用该枚举的代码。添加一个另一方面,向枚举新操作完全是自包含的,因为所有需要处理的情况都就在那里(与接口和超类进行对比,在其中添加非默认方法是一个重大的突破性变化)。这取决于真正必要的更改类型。

1
关于MusicXML:如果没有简单的方法可以从XML文件中分辨出每个用户使用的是哪个版本的架构,那么这将成为规范中的关键设计缺陷。如果您必须以某种方式解决该问题,那么直到我们确切知道他们将选择在4.0中破解并且您可以询问它导致的特定问题之前,我们可能无法提供帮助。
Ixrec

Answers:


25

如果使用正确,枚举要比其替换的“幻数”更具可读性和鲁棒性。我通常不会看到它们使代码更脆弱。例如:

  • setColor()不必浪费时间检查是否value为有效的颜色值。编译器已经做到了。
  • 您可以编写setColor(Color :: Red)而不是setColor(0)。我相信enum class现代C ++中的功能甚至可以让您强迫人们始终编写前者而不是后者。
  • 通常,它并不重要,但是大多数枚举都可以用任何大小的整数类型实现,因此编译器可以选择最方便的大小,而不必强迫您考虑这些事情。

但是,使用枚举来表示颜色是有问题的,因为在许多(大多数?)情况下,没有理由将用户限制在如此小的颜色范围内。您也可以让它们传递任意的RGB值。在我从事的项目中,只有一小部分这样的颜色只能作为一组“主题”或“样式”的一部分出现,而这些主题或样式应作为对具体颜色的抽象。

我不确定您的“多态灵活性”问题是什么。枚举没有任何可执行代码,因此没有使多态的东西。也许您在找命令模式

编辑:编辑后,我仍不清楚您要寻找哪种扩展性,但我仍然认为命令模式是您将最接近“多态枚举”的东西。


在哪里可以找到有关不允许0作为枚举通过的更多信息?
TankorSmash

5
@TankorSmash C ++ 11引入了“枚举类”,也称为“作用域枚举”,它不能隐式转换为其基础数字类型。他们还避免像旧的C样式“枚举”类型那样污染名称空间。
马修·詹姆斯·布里格斯

2
枚举通常由整数支持。在整数和枚举之间的序列化/反序列化或转换可能会发生很多奇怪的事情。假定枚举始终具有有效值并不总是安全的。
埃里克

1
您是正确的Eric(这是我本人多次遇到的问题)。但是,您只需要担心反序列化阶段中可能无效的值。在所有其他使用枚举的时间中,都可以假定该值是有效的(至少对于Scala之类的语言中的枚举-某些语言对枚举的类型检查不很强)。
2015年

1
@Ixrec“因为在很多(大多数?)情况下,没有理由将用户限制为如此小的颜色集”在某些情况下是合理的。在.NET控制台模仿老式窗控制台,它只能有16种颜色(广汇标准16色)一文msdn.microsoft.com/en-us/library/...
Pharap

15

对ColorChoice枚举的任何更改都会影响所有IWindowColor子类。

不,不是。有两种情况:实施者要么

  • 存储,返回和转发枚举值,从不对其进行操作,在这种情况下,它们不受枚举更改的影响,或者

  • 个别枚举值,在这种情况中当然枚举必须的任何变化,自然,不可避免地,操作一定,来解释在实施者的逻辑的相应变化。

如果您将“球形”,“矩形”和“金字塔”放入“形状”枚举中,并将该枚举传递给drawSolid()您编写的用于绘制相应实体的某个函数,然后某个早晨您决定添加“ Ikositetrahedron“值作为枚举,您不能指望该drawSolid()功能保持不受影响;如果您确实希望它以某种方式绘制icositetrahedrons,而无需首先编写实际代码来绘制icositetrahedrons,那是您的错,而不是枚举的错。所以:

枚举会导致界面脆弱吗?

不,他们没有。导致脆弱接口的原因是程序员将自己视为忍者,试图在未启用足够警告的情况下编译其代码。然后,编译器不会警告他们其drawSolid()函数包含一条switch语句,该语句缺少case新添加的“ Ikositetrahedron”枚举值的子句。

它的工作方式类似于将一个新的纯虚方法添加到基类:然后,您必须在每个单个继承程序上实现此方法,否则项目将(也不应构建)。


现在,说实话,枚举不是面向对象的构造。它们更多地是在面向对象的范例与结构化的编程范例之间的务实的折衷。

纯粹的面向对象的处理方式根本没有枚举,而是完全具有对象。

因此,使用实体实现示例的纯粹面向对象的方法当然是使用多态性:您无需声明一个单一的,集中式的方法就知道如何绘制所有内容,而必须告知要绘制的实体,而是声明一个“实体”类,并使用抽象(纯虚拟)draw()方法,然后添加“球体”,“矩形”和“金字塔”子类,每个子类都有其自己的实现,draw()这些子类都知道如何绘制自身。

这样,当您引入“ Ikositetrahedron”子类时,只需要为其提供一个draw()函数,编译器就会提醒您这样做,否则不要实例化“ Icositetrahedron”。


关于为这些开关引发编译时警告的任何提示?我通常只抛出运行时异常。但是编译时间可能很棒!不太好..但是想到了单元测试。
沃恩·希茨

1
自从我上次使用C ++以来已经有一段时间了,所以我不确定,但是我希望向编译器提供-Wall参数并省略该default:子句就可以了。快速搜索该主题没有得到更多确定的结果,因此这可能适合作为另一个programmers SE问题的主题。
Mike Nakis

在C ++中,如果启用它,则可以进行编译时检查。在C#中,任何检查都必须在运行时进行。每当我将非标志枚举用作参数时,请确保使用Enum.IsDefined对其进行验证,但这仍然意味着您必须在添加新值时手动更新枚举的所有用法。请参阅:stackoverflow.com/questions/12531132/...
迈克支持莫妮卡

1
我希望面向对象的程序员永远不要使他们的数据传输对象成为对象,就像Pyramid实际上知道如何draw()金字塔一样。充其量,它可能源自Solid并具有GetTriangles()方法,您可以将其传递给SolidDrawer服务。我以为我们正在摆脱物理对象的示例作为OOP中的对象的示例。
Scott Whitlock'3

1
没关系,只是不喜欢有人抨击旧的面向对象编程。:)尤其是因为我们大多数人都比严格的OOP更加结合了函数式编程和OOP。
斯科特·惠特洛克

14

枚举不会创建脆弱的接口。枚举的滥用确实存在。

枚举有什么用?

枚举旨在用作有意义命名的常量集。它们将在以下情况下使用:

  • 您知道不会删除任何值。
  • (而且)您知道极不可能需要新值。
  • (或)您接受了将需要一个新值的要求,但是这种新值很少足以保证修复所有由于它而损坏的代码。

枚举的良好用法:

  • 一周中的几天:(按照.Net的规定System.DayOfWeek),除非您要处理一些难以理解的压光机,否则一周中只有7天。
  • 不可扩展的颜色:(根据.Net的规定System.ConsoleColor)有些人可能对此表示不同,但.Net选择这样做是有原因的。在.Net的控制台系统中,控制台只能使用16种颜色。这16种颜色对应于称为CGA或“彩色图形适配器”的传统调色板。永远不会添加任何新值,这意味着这实际上是枚举的合理应用。
  • 代表固定状态的枚举:(按照Java的规定Thread.State)Java的 设计者认为,在Java线程模型中,将永远只有a可以处于的一组固定状态, Thread因此为了简化起见,这些不同的状态被表示为枚举。 。这意味着许多基于状态的检查都是简单的if和switch,它们实际上在整数值上运行,而程序员不必担心这些值实际上是什么。
  • 表示非互斥选项的位System.Text.RegularExpressions.RegexOptions标记:(根据.Net的)位标记是枚举的非常常见的用法。实际上如此普遍,以至于.Net中所有的枚举都有一个HasFlag(Enum flag)内置的方法。它们还支持按位运算符,并且有一个FlagsAttribute将枚举标记为打算用作一组位标志的标记。通过将枚举用作一组标志,您可以在单个值中表示一组布尔值,并且为了方便起见,对这些标志进行了明确命名。这对于在仿真器中表示状态寄存器的标志或表示文件的权限(读取,写入,执行),或几乎所有相关选项不是互斥的情况,都是非常有益的。

枚举的错误用法:

  • 游戏中的角色类别/类型:除非游戏是一次性演示,您不太可能再次使用,否则枚举不应用于角色类别,因为您可能会希望添加更多的类别。最好有一个代表角色的类,而用其他方式代表游戏中的角色“类型”。一种解决此问题的方法是TypeObject模式,其他解决方案包括在字典/类型注册表中注册字符类型,或二者结合。
  • 可扩展的颜色:如果您将枚举用于以后可能添加的颜色,则以枚举形式表示它不是一个好主意,否则您将永远被添加颜色。这与上面的问题类似,因此应使用类似的解决方案(即TypeObject的变体)。
  • 可扩展状态:如果您拥有的状态机可能最终引入了更多状态,那么通过枚举表示这些状态不是一个好主意。首选方法是为机器状态定义一个接口,提供一个包装该接口并委托其方法调用的实现或类(类似于 Strategy模式),然后通过更改当前提供的实现处于活动状态来更改其状态。
  • 表示互斥选项的位标志如果您使用枚举来表示标志,并且其中两个标志永远不应该同时出现,则说明您已陷入困境。被编程为对其中一个标志作出反应的任何内容都会突然对它被编程为首先响应的任何标志作出反应-或更糟糕的是,它可能对两个标志都作出响应。这种情况只是自找麻烦。最好的方法是在可能的情况下将不存在标志视为替代条件(即,不存在True标志意味着False)。通过使用专用功能(即IsTrue(flags)IsFalse(flags))可以进一步支持此行为。

如果有人可以找到用作枚举的枚举的有效示例或众所周知的示例,我将添加该示例。我知道它们的存在,但是很遗憾,我目前无法回忆起任何事情。
法拉普


@BLSully优秀的例子。不能说我曾经使用过它们,但是它们存在是有原因的。
法拉普2015年

4

对于没有太多功能关联的封闭值集,枚举是对魔术标识号的极大改进。通常,您并不关心与枚举实际关联的数字;在这种情况下,很容易通过在末尾添加新条目来进行扩展,因此不会造成任何脆性。

问题是当您具有与枚举关联的重要功能时。也就是说,您周围有这种代码:

switch (my_enum) {
case orc: growl(); break;
case elf: sing(); break;
...
}

这些中的一两个是可以的,但是一旦有了这种有意义的枚举,这些switch语句就会像三边形一样迅速繁殖。现在,每次扩展枚举时,都需要查找所有关联的switches以确保已覆盖所有内容。很脆。诸如“ 清洁代码”之类的资料表明,switch每个枚举最多应有一个。

在这种情况下,您应该使用OO原理并为此类型创建一个接口。您仍然可以保留枚举来传达这种类型,但是一旦您需要对其进行任何操作,就可以创建一个与枚举关联的对象,可能使用Factory。这很容易维护,因为您只需要查找一个要更新的地方:Factory,并添加一个新类即可。


1

如果您谨慎地使用它们,我不会认为枚举有害。但是,如果要在某些库代码中使用它们而不是单个应用程序,则需要考虑一些事项。

  1. 切勿删除或重新排序值。如果您在列表中的某个时刻有一个枚举值,则该值应与该名称永久关联。如果需要,可以在某个时候将值重命名为deprecated_orc或其他名称,但是通过不删除它们,可以更轻松地保持与针对旧枚举集编译的旧代码的兼容性。如果某些新代码无法处理旧的枚举常量,请在该位置生成适当的错误,或者确保没有任何值到达该代码段。

  2. 不要对它们进行算术运算。特别是,请勿进行订单比较。为什么?因为那样的话,您可能会遇到无法保留现有值并不能同时保留合理顺序的情况。以指南针方向的枚举为例:N = 0,NE = 1,E = 2,SE = 3,...现在,如果在进行一些更新后在现有方向之间包括NNE等,则可以将其添加到末尾列表的顺序,从而破坏顺序,或者将它们与现有键交错,从而破坏在旧代码中使用的已建立映射。或者,您不赞成使用所有旧密钥,并拥有一组全新的密钥,以及一些兼容性代码,这些代码出于遗留代码的缘故在新旧密钥之间进行转换。

  3. 选择足够的尺寸。默认情况下,编译器将使用可包含所有枚举值的最小整数。这意味着,如果在某些更新中,可能的枚举集从254扩展到259,则每个枚举值突然需要2个字节,而不是一个字节。这可能会破坏整个位置的结构和类布局,因此请尝试在第一个设计中使用足够的大小来避免这种情况。C ++ 11在这里为您提供了很多控制,但是指定一个条目也LAST_SPECIES_VALUE=65535应该有所帮助。

  4. 有一个中央注册表。由于您要修复名称和值之间的映射,因此,如果让代码的第三方用户添加新的常量,那是不好的。不允许使用您的代码的项目更改该标头以添加新的映射。相反,它们应该使您烦恼以添加它们。这意味着对于您提到的人类和侏儒的例子,枚举确实不适合。最好在运行时拥有某种注册表,代码的用户可以在其中插入字符串并获取一个唯一的数字,该数字很好地包裹在某种不透明的类型中。“数字”甚至可能是指向所讨论字符串的指针,这无关紧要。

尽管事实上我在上面的最后一点使枚举不适合您的假设情况,但是您的实际情况(某些规范可能会更改,并且您必须更新一些代码)似乎非常适合您的库的中央注册表。因此,如果您将我的其他建议牢记在心,那么枚举应该在那里适当。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.