65.9K
CodeProject 正在变化。 阅读更多。
Home

枚举类型不枚举!.NET 和语言限制的变通方法

2010年11月22日

CPOL

31分钟阅读

viewsIcon

280673

downloadIcon

664

用于枚举式迭代和数组索引的泛型类。

EnumTypesDoNotEnumerate/Enumerations.png

引言

我所代表的科学给出了一个严格且明确的答案:*也许*。

A & B Strugatsky, 三套马车的故事

目录

1 引言

本文是我向 CodeProject 会员提交的关于枚举类型的小系列文章中的第一篇。

  1. 本文;
  2. 人类可读的枚举元数据,*枚举成员的显示名称和描述:一种非侵入性的可靠本地化方法*;
  3. 基于枚举的命令行实用程序;
  4. 用于 PropertyGrid 和 Visual Studio 的位枚举编辑器.

枚举类型的限制根植于 .NET 平台架构,而非我代码示例和解决方案实现所使用的 C# 语言。尽管如此,出于两个相关的原因,我更倾向于称之为语言限制。首先也是最重要的,平台的限制可以通过语言特性来弥补;在所有情况下,最终都要归咎于语言本身表达能力不足,尽管一种语言无与伦比的表达能力会导致与其他语言的兼容性问题(例如,比较 F# 和其他 .NET 语言)。其次,像表示 .NET 类库的 API 这样的任何 API,都可以被视为广义上的语言。

本工作的目的是通过提供三个泛型类型来弥补所需功能的缺失:一个将允许有效地将任何枚举类型转换为支持完整枚举语义的可枚举容器,以迭代其类型参数的 `static` 成员;另外两个将允许数组索引。这两种功能都以小巧的占位符和合理的性能实现。

考虑一个简单的 `enum` 声明(摘自维基百科关于枚举类型的文章)

enum CardSuit { Clubs, Diamonds, Spades, Hearts, }

以下内容看起来不是一个备受期待的功能吗?

// will not compile:
foreach(CardSuit item in CardSuit) {
    // use item...
} //loop

当我们尝试理解为什么 `foreach` 语句无法与我们的枚举一起工作时,我们会发现它需要工作集实现 `IEnumerable`。这是一个令人震惊的事实,`enum` 类型不实现 `IEnumerable`。

枚举类型是不可枚举的!

这个事实很难理解,因为枚举的能力可以被认为是枚举类型固有的特征,正如术语本身所暗示的那样。在某些其他语言中,例如 AdaDelphi Pascal(C# 和 .NET 的直接前身;Delphi 2005 引入了 foreach 支持)、Java 等语言中,这种能力得到了很好的支持。

更重要的是,将枚举类型用作数组索引也非常自然。考虑以下 Pascal 声明

suitCount: array[CardSuit] of integer;

ANSI C 中枚举的引入产生了一系列语法相似的语言,这些语言缺乏由于其属性和操作集而对这种结构而言自然的、功能完备的枚举语义。本质上,在这些语言中被称为枚举或 `enum` 的东西并不代表真正的枚举类型。当考虑 CC++ 时,这种事实尚可理解:在具有过时链接和 `include` 结构的这些语言中,枚举类型仅充当一组整数常量,通过单点声明使用,消除了对单独值定义的需求。对于 C# 等更现代的语言,缺乏枚举语义似乎只是 C 祖先的又一个历史遗留问题。

2 背景:`Enumeration` 类之前的编程生活

新的枚举和索引功能基于泛型类 `Enumeration`,该类设计用于作为基于枚举类型的可枚举容器。另外两个泛型类基于 `Enumeration` 的功能,并用于枚举索引数组:`EnumerationIndexedArray` 和 `CartesianSquareIndexedArray`。

在提供我的解决方案之前,我需要提供一些关于 .NET 枚举类型的背景信息。尽管如此,我不想做得太全面。相反,我将通过主要与迭代某些枚举值集相关的代码片段来说明枚举工作,并讨论相关问题。

我理解并非所有人都需要使用我的解决方案,毕竟额外的代码就是额外的麻烦。我也不想夸大我全面解决方案的重要性,但我想让我的读者将其与一些更简单的临时技术进行比较,以便能够做出正确的选择。因此,我将从解释那些更简单的技术开始,标题为“`Enumeration` 类之前的编程生活”。

2.1 最简单的迭代循环

鉴于上面对`CardSuit` 的声明,迭代循环可以写得很简单

for (CardSuit loopVariable = CardSuit.Clubs;
    loopVariable <= CardSuit.Hearts;
    loopVariable++) {
    // use loopVariable...
} //loop CardSuit
for (CardSuit loopVariable = CardSuit.Hearts;
    loopVariable >= CardSuit.Clubs;
    loopVariable--) {
    // use loopVariable...
} //loop CardSuit in reverse

这段代码片段表明,比较、递增和递减运算符适用于枚举,因此无需将枚举值转换为其整数表示并反之。

这段代码的主要问题是可维护性。如果这段代码的目的是迭代完整的枚举成员集,那么当在现有声明末尾添加新的 `CardSuit` 成员或在开头插入时,它将中断。如果删除了 `Hearts` 或 `Clubs` 成员,代码将无法编译,这更容易,因为它会立即检测到问题。在所有情况下,枚举类型声明的任何修改都需要审查其所有用法,这足以称之为维护噩梦。

2.2 使用长度描述符

有一种简单的技术用于缓解上述可维护性问题。这项技术在 C/C++ 开发者中非常有名。让我们在 `CardSuit` 声明中添加一个辅助成员

enum CardSuit {
    //semantic members:
    Clubs = 0, Diamonds, Spades, Hearts,
    //auxiliary member: length descriptor
    Length,
} //enum CardSuit

假设第一个成员的底层整数值始终为零,这可以解决添加和删除“语义”成员的问题。

for (CardSuit loopVariable = 0;
    loopVariable < CardSuit.Length;
    loopVariable++) {
    // use loopVariable...
} //loop CardSuit

但是,这使得反向迭代变得有点复杂

CardSuit length = CardSuit.Length;
for (CardsuitWithLengthDescriptor loopVariable = length--;
    loopVariable >= 0;
    loopVariable--) {
    // use loopVariable...
} //loop CardSuit in reverse

用于计算循环变量初始值的第一个语句是必需的,因为递减运算符可以应用于变量,但不能应用于 `static` 字段。

当然,另一种迭代方式是使用整数循环变量而不是枚举,并结合适当的类型转换。它可以用于所有类型的迭代,并使反向迭代稍微简单一些

for (int loopVariable = (int)CardSuit.Length - 1;
    loopVariable >= 0;
    loopVariable--) {
    CardSuit loopItem =
        (CardSuit)loopVariable;
    // use loopItem...
} //loop with int

这有更好吗?我不这么认为。尽管它简化了反向迭代的外观,但使可维护性问题更糟。其中一个原因是整数类型:枚举声明语法允许指定底层整数类型。此类型应与循环变量的类型同步。谁会自愿负责这一点?

2.3 使用最大值描述符

上述迭代技术可以稍微泛化,允许迭代成员的最小值大于零。它还使反向迭代更简单,使前向和反向迭代看起来是对称的。为了实现这一点,让我们添加两个额外的辅助成员

enum CardSuit {
    //semantic members:
    Clubs, Diamonds, Spades, Hearts,
    //auxiliary members:
    Length, //inconvenient for reverse iteration, optional
    First = 0, //or First = Clubs, less supportable way
    Last = Length - 1, //or Last = Hearts, less supportable way
} //enum CardsuitDomain

for (CardSuit loopVariable = CardSuit.First;
    loopVariable <= CardSuit.Last;
    loopVariable++) {
    // use loopVariable...
} //loop CardSuit
for (CardSuit loopVariable = CardSuit.Last;
    loopVariable >= CardSuit.First;
    loopVariable--) {
    // use loopVariable...
} //loop CardSuit in reverse

上面的迭代循环代码片段允许在不查看使用它的代码的情况下对 `CardSuit` 声明进行一定类别的修改:可以添加或删除任何“语义”成员,声明的第一个成员可以赋值为非零整数值(`First` 的值必须同步修改)。此外,还可以轻松地进一步泛化声明,以包含未参与迭代的额外的“语义”枚举成员。

但是,在所有情况下,只有当迭代是在一组连续的整数值上执行时,迭代才会如预期那样工作。

2.4 迭代位集

枚举类型的按位运算符非常方便,并且经常用于实现集合代数和更复杂的计算。为此,枚举类型基本上使用 2 的幂的底层整数值作为其成员,有时与其他成员结合使用。

对于某些应用程序,遍历位集的位(例如,自动生成每个位标志的文档)是可取的。上述迭代技术要求底层整数值是连续的,因此它们不能直接使用此类枚举类型。

辅助的“位位置”枚举类型有助于绕过此限制。让我们考虑(非常简化)文本搜索选项的示例

enum SearchOptionBitPosition {
    MatchCase, //otherwise case-insensitive
    SearchBackward, //otherwise search forward
    UseRegularExpressions,
    WholeText, //otherwise start from cursor
    // auxiliary member: length descriptor
    Length,
} //enum SearchOptionBitPosition

[System.Flags] enum SearchOptions {
    Default = 0,
    MatchCase =
      1 << SearchOptionBitPosition.MatchCase,
    SearchBackward =
      1 << SearchOptionBitPosition.SearchBackward,
    UseRegularExpressions =
      1 << SearchOptionBitPosition.UseRegularExpressions,
    WholeText =
      1 << SearchOptionBitPosition.WholeText,
} //enum SearchOptions

//...

SearchOptions options =
    SearchOptions.MatchCase | SearchOptions.UseRegularExpressions;
bool caseSensitive = (options & SearchOptions.MatchCase) > 0;
bool ignoreCase = (options & SearchOptions.MatchCase) == 0;

//...

for (SearchOptionBitPosition loopVariable = 0;
     loopVariable <= SearchOptionBitPosition.Length;
     loopVariable++) {
    SearchOptions option = (SearchOptions)(1 << (int)loopVariable);
    // use option and loopVarible
} //loop CardSuit

当然,像双重类型转换这样的事情可能会导致错误。然而,这种方法最大的问题是位位置必须是连续的值。在许多情况下,这些值也不是连续的;有时它们无法修改,因为它们来自第三方库。这对于通过 P/Invoke 使用的非托管枚举类型来说是很典型的。

2.5 模拟扩展式编程

到目前为止,我对 C#/.NET 枚举背景的介绍仅限于以下范围:我承诺通过我开发的泛型类来解决到目前为止描述的每一个问题。

在本节中,我想讨论另一个与迭代无关的枚举问题。不幸的是,我找不到任何足够好的解决方案值得去研究。我只想借此机会展示并讨论一种最简单的技术来结束这个话题。当然,如果有人能给我一个更好的主意,我将非常感激。

问题在于缺乏扩展机制(如继承)或基于值类型创建派生类型的任何机制。在许多众所周知的场景中会使用派生值类型(尤其是在 Ada 中),但对于枚举类型,扩展看起来尤其诱人。想象一下以下声明

enum CommandSet { New, Open, Save, SaveAs, }
// will not compile:
enum ExtendedCommandSet : CommandSet { Import, Export, }

理想情况下,`ExtendedCommandSet` 的变量应与任何 `CommandSet` 值赋值兼容,反之则不然。

我只能给出以下技术建议

enum ExtendedCommandSet {
    // "inherited" members:
    New = CommandSet.New,
    Open = CommandSet.Open,
    Save = CommandSet.Save,
    SaveAs = CommandSet.SaveAs,
    // "new" members:
    Import, Export,
} //enum ExtendedCommandSet

请注意,在 `ExtendedCommandSet` 声明中使用来自不同枚举类型作用域的值进行的赋值允许在没有类型转换的情况下进行,这与赋值运算符不同。仔细想想,这是一个非常方便但又安全的设计特性。

不幸的是,这两种类型之间的赋值始终需要类型转换,但这种技术保证了预期的值。模拟扩展的声明仍然有点繁琐,但如果将它们放在一起,则很容易使其与“`base`”类型保持同步。

另一个问题可能是底层整数类型不兼容。例如,以下声明将无法编译

enum CommandSet { //will compile:
    New = -1,
    Open = int.MinValue,
    Save = int.MaxValue,
    SaveAs = byte.MaxValue + 1,
} //enum CommandSet
enum ExtendedCommandSet : byte {
    // next 4 members will not compile;
    // "cannot be converted to a byte":
    New = CommandSet.New,
    Open = CommandSet.Open,
    Save = CommandSet.Save,
    SaveAs = CommandSet.SaveAs,
    Import, Export,
} //enum ExtendedCommandSet

如果“派生”类型的底层整数类型与“基”枚举类型的底层整数类型相同或更宽,则此问题不会发生。

Nick Polak 在他关于使用 Roslyn 的 Visual Studio 扩展的文章《使用 Roslyn 实现枚举继承》中,提出了一种基于他的 Visual Studio 插件来支持枚举类型“继承”模仿的有趣方法。

2.6 迭代问题

尽管上面展示的所有技术都有效,但代码看起来很丑陋。此外,该方法还需要特别小心以避免偏移量错误

仅处理一组连续值是一个严重的限制。本质上,这会将技术限制在那些成员整数值未指定且无关紧要的枚举类型。再次,有时甚至无法使用连续值(参见 2.4)。

使用迭代需要相当大的编码纪律,以使“语义”成员和“辅助”成员分开并保持同步。一种可能的方法是确保这些“辅助”成员的名称在整个项目(甚至开发者团队)中都保留其用途。显然,此类代码的可维护性仍然不完美。

出于某种原因,C# 枚举也缺少所有数字类型中常见的必备属性,如 `MinValue` 和 `MaxValue` 成员。同样,这很难解释,因为对于每个非空枚举类型,这些值始终存在;任何人都可以通过调用 `System.Enum.GetValues` 方法来获取它们。(非空假设很重要:对于像“`enum Empty {}`”这样的类型,无法定义 `MinValue` 或 `MaxValue` 值,但这些成员可以实现为 `static` 属性,在枚举为空时引发异常。)Needless to say, `MinValue` 和 `MaxValue` 成员将极大地促进迭代。

还有一个更根本的问题,与迭代没有直接关系。如果我们尝试获取枚举成员的名称,例如使用 `CardSuit.First.ToString()`,会发生什么?见下文……

2.7 枚举成员名称的问题

2.3 中提出的 `CardSuit` 形式的声明可用于说明通过其值获取枚举成员名称的问题。在物理上,枚举值由其底层整数值表示。一种检索名称的方法是调用 `object.ToString` 方法,例如

string first = CardSuit.First.ToString();

获取名称的另一种方法是调用 `enum.GetName` 方法;可以通过调用 `enum.GetNames` 方法返回枚举类型的名称数组。

正如我们所知,`CardSuit.First == CardSuit.Clubs`;也就是说,这是多个枚举成员具有相同底层值的情况;那么字符串 `first` 的值是多少?

我花了一段时间为 .NET Platform v. 3.5 和 4 编译进行实验。我发现,在我尝试过的所有情况下,此方法都返回之前声明的枚举成员的名称;在本例中,是 `"Clubs"`。然而,我隐约记得,在 .NET Platform v.2.0 中,我有时观察到相反的情况:`CardSuit.Clubs.ToString() == "First"`。我观察到调用 `enum.GetName` 或 `enum.GetNames` 也有类似的结果。这是否意味着此 API 不保证此调用的特定结果?(这就是我选择上述引文作为本文的引言的原因。)

可以在 MSDN .NET 4 中关于 `enum.GetName` 方法的文档中找到此猜测的确认。

“如果多个枚举成员具有相同的底层值,`GetName` 方法保证它将返回这些枚举成员之一的名称。但是,它不保证它将始终返回相同的枚举成员的名称。因此,当多个枚举成员具有相同的值时,您的应用程序代码永远不应依赖于该方法返回特定成员的名称。”

关于 `ToString` 的文档使用示例代码传达了相同的思想。至于 `GetNames` 方法,它看起来稍微复杂一些

“返回值数组的元素按枚举常量的值的顺序排序。如果存在具有相同值的枚举常量,则其对应名称的顺序是不确定的。”

该方法应该返回多少个名称,以及是否可以预期重复的 `string` 值,这一点并未明确说明。我最近对 `CardSuit` 的实验表明,返回了所有 7 个 `string`;而且它们都不同。总之,我不会依赖它。

实际上,所有三种方法返回的结果在一般情况下都不能被许多应用程序认为是可靠的。

这是一个问题。简单地说“避免使用同一枚举类型的不同成员具有相同的底层整数值——这是一种不良做法”不是一个有效的解决方案。在某些情况下,使用此类成员是不可避免的。典型情况是使用某些外部 API,尤其是非托管 API。在这种情况下,

  1. 底层整数值确实很重要,并且
  2. 经常使用相同的值。

例如,为什么 `System.Windows.Forms.MessageBoxIcon.Error` 和 `System.Windows.Forms.MessageBoxIcon.Hand` 具有相同的底层整数值?因为出于历史原因,它们在非托管 Windows API 中被定义为如此,并且因为 `System.Windows.Forms.MessageBox` 功能依赖于此 API。另一个例子是使用“辅助”成员来实现 2.3 中所述的目的。

请注意,我基于 `Enumeration` 类的解决方案不依赖于这三种方法中的任何一种。相反,枚举成员信息通过反射获取并以详细形式存储(参见 3.1`EnumerationItem`)。这种方法完全没有歧义(参见 3.14)。

3 前景:使用 `Enumeration` 类和枚举索引数组

上一节致力于我们如果不使用我提供的 `Enumeration` 类而必须使用的技术。既然我已经称该节为“背景”,我只能将下一节称为“前景”,并最终解释我提出的解决方案。我将首先通过用法开始解释,然后再给出如何工作的想法(4)。背景 部分概述的问题应该足以证明以全面方式解决这些问题的合理性。

首先描述的是泛型类 `Enumeration<ENUM>` 及其用法模式。

3.1 枚举什么?

首先,我们需要了解我们真正想要什么。这本质上是一组枚举项和枚举的顺序。如果我们考虑具有任意整数值的枚举成员,这似乎并非易事。我们不应该假定这些整数值的顺序遵循其类型声明中枚举成员的声明顺序,更不用说限制为连续值了。此外,我们不应该假定枚举成员名称与其各自整数值之间存在一对一的对应关系。将枚举成员映射到其整数值的函数总是满射但不总是单射:对于每个枚举成员,它返回一个且只有一个相应的整数值,但相同的整数值可以返回给多个枚举成员。

由于无法强制执行有关枚举成员对应底层整数值的任何规则,因此对这些值的任何假设都会破坏 `Enumeration` 类的功能。

实际上,每个枚举成员(但不是枚举类型的变量)都关联着两个不同的整数值:第一个是该成员在源代码中声明的零基排序号;我称之为*“自然顺序”*;第二个是成员的底层整数值。这两个数字并不总是相等的。枚举成员的完整运行时信息(间接包括其自然排序号)保存在程序集元数据中,并由 `FieldInfo` 类型实例表示。至于枚举类型的变量,在运行时它仅由其底层整数值表示;其未装箱形式的大小与其类型的底层整数类型的大小完全相同。因此,并不总是可以通过枚举类型变量中存储的值找到枚举成员。这是 2.7 中描述的枚举成员歧义问题的根本原因。

考虑到所有这些,只能得出一个结论:迭代应按*自然顺序*(可选地反向)进行。底层整数值不应影响通过迭代获得的对象的集合,也不应影响迭代的顺序。相反,循环变量的类型应提供有关枚举项的所有有用信息。

这是类型声明

public sealed class EnumerationItem<ENUM> {
    private EnumerationItem() { }
    // ...internal constructor
    public string Name { get { /* ... */ } }
    public Cardinal Index { get { /* ... */ } }
    public ENUM EnumValue { get { /* ... */ } }
    public object Value { get { /* ... */ } }
    // ...implementation
} //struct EnumerationItem

请注意,此类的实例无法直接构造(除非使用基于反射的技巧)。它仅在 `Enumeration` 类内部构造。

类型 `Cardinal` 通过 `using` 指令定义为 `System.UInt32` 的别名。`Index` 属性提供了枚举项的索引,该索引按*自然顺序*生成(不包括标记有 `NonEnumerable` 属性的项,见下文)。此属性与分配给相应枚举成员的整数值无关;如果需要此整数值,则可以将其作为 `EnumValue` 属性的类型转换值来获得。

`Name` 属性提供相应枚举成员的名称。此名称由其在枚举声明中出现的精确成员名称定义,并且没有 2.7 中描述的名称歧义问题,因此它并不总是与 `EnumValue.ToString()` 返回的字符串相同。

两个单独的属性 `EnumValue` 和 `Value` 的目的将在 3.3 中解释。

某些枚举成员可以从枚举对象集中排除。为此,可以使用以下属性

[AttributeUsage(
    AttributeTargets.Field,
    AllowMultiple = false,
    Inherited = false)]
public class NonEnumerableAttribute : Attribute { }

3.2 `Enumeration` 的使用

考虑到 `CardSuit` 的声明之一,该集合可以这样迭代

Enumeration<CardSuit> cardSuiteEnumeration
    = new Enumeration<CardSuit>();
foreach (EnumerationItem<CardSuit> item in cardSuiteEnumeration) {
    // use item
} //loop CardSuit

要更深入地了解 `EnumerationItem`、其底层枚举类型及其成员之间的关系,您可以尝试进行如下操作

Type underlyingIntegerType =
    Enum.GetUnderlyingType(typeof(CardSuit));
foreach (EnumerationItem<CardSuit> item in cardSuiteEnumeration) {
    object intValue = Convert.ChangeType(
        item.EnumValue,
        underlyingIntegerType);
    WriteLine(
        "{0}={1}={2}",
        item.Name,
        item.EnumValue,
        intValue);
} //loop CardSuit

这是一个相当高级的代码片段,演示了如何在不事先了解确切整数类型的情况下获取有关枚举项的所有基本信息,包括其底层整数值。

如果出于某种原因,枚举声明使用了某些辅助成员(例如,为了向后兼容早期创建的代码而未删除 2.2 中定义的`Length` 描述符),则应修改声明以将其排除在迭代序列之外。

enum CardSuit {
    // semantic members:
    Clubs, Diamonds, Spades, Hearts,
    // auxiliary member: length descriptor
    [NonEnumerable] Length,
} //enum CardSuit

要获取反向迭代顺序,可以使用 `IsReverse` 属性

cardSuiteEnumeration.IsReverse = true;

3.3 如果 `ENUM` 不是枚举类型怎么办?

现在,是时候看看 `Enumeration` 类的声明了

public class Enumeration<ENUM> :
        IEnumerable<EnumerationItem<ENUM>> {
    public Enumeration() { /* ... */ }
    public static Cardinal CollectionLength {
        get { /* ... */ }
    } // CollectionLength
    public static ENUM First {
        get { /* ... */ }
    } //First
    public static ENUM Last {
        get { /* ... */ }
    } //Last
    public EnumerationItem<ENUM> this[Cardinal index] {
        get { /* ... */ }
    public bool IsReverse {
        get { /* ... */ }
        set { /* ... */ }
    } // IsReverse
    //...
 } //class Enumeration

一切看起来都很清楚……等一下!我们如何确保类型参数始终是枚举类型,即派生自 `System.Enum`?问题是,不允许使用特殊类型 `System.Enum` 作为泛型参数类型约束。

当这个问题开始困扰我时,我突然意识到……使用基于任何其他类型(值类型或引用类型、原始类型或非原始类型)的 `Enumeration` 类绝对没有问题。

例如,让我们对 `double` 类型应用相同的迭代循环

static Enumeration<double> doubleEnum
    = new Enumeration<double>();
foreach (EnumerationItem<double> item in doubleEnum)
    WriteLine(
    "{0}={1}",
    item.Name, item.Value);

输出如下

MinValue=-1.79769313486232E+308
MaxValue=1.79769313486232E+308
Epsilon=4.94065645841247E-324
NegativeInfinity=-Infinity
PositiveInfinity=Infinity 
NaN=NaN

原因如下:`Enumeration` 功能不使用关于 `ENUM` 类型性质的任何知识,无论它是否是枚举类型。基本上,它会收集有关 `ENUM` 类型所有 `public static` 字段的信息(4.1)。例如,每个浮点数值类型都定义了上面 `double` 类型所示的五个 `public static` 字段。

枚举类型中唯一考虑的特殊功能是:对于每个枚举类型,其所有 `public static` 字段都保证是同一类型。这对于某些其他类型(如所有数字类型)也适用,但通常情况并非如此。这种差异用于不同地分配 `EnumerationItem.EnumValue`(`ENUM` 类型)和 `EnumerationItem.Value`(`object` 类型)属性使用的值。

如果 `ENUM` 是枚举类型,则所有枚举成员的两个属性都属于同一类型。如果 `ENUM` 不是枚举类型,则取决于类型的每个单独的 `public static` 字段:如果字段与 `ENUM` 类型相同,则 `EnumerationItem` 实例的创建方式与枚举类型完全相同,否则只有 `EnumerationItem.Value` 的值包含实际字段值;在这种情况下,`EnumerationItem.EnumValue` 的值没有信息量(使用 `default` 运算符赋值)。

3.4 关于标志和按位运算的说明

通过枚举成员按位运算获得的值(参见 2.4)对于可枚举功能或数组索引没有特殊含义。如果该值碰巧等于枚举类型的声明的底层值之一或多个枚举成员,它将出现在某些迭代中,或可用于索引数组元素。

`System.FlagsAttribute` 可与枚举类型一起使用。它只能影响 `ToString` 或 `System.Enum.GetName` 方法返回的枚举值的名称,并且旨在与按位运算符一起使用。由于当前代码中未使用这些方法中的任何一种,因此此标志对迭代或数组索引的结果没有影响。

所有这些都是上述通用行为的简单结果。枚举类型的这些方面通常会引起许多问题和争议,因此我试图预见它们。

3.5 数组索引

数组索引的使用看起来与常规数组非常相似

EnumerationIndexedArray<CardSuit, double> rating
    = new EnumerationIndexedArray<CardSuit, double>();
EnumerationIndexedArray<CardSuit, double> defaultRating
    = new EnumerationIndexedArray<CardSuit, double>(1.0d);
rating[CardSuit.Clubs] = 3 / 4;
double defaultValue =
   defaultRating[CardSuit.Diamonds]; // returns 1.0;
rating[CardSuit.Length] = 3.1; // throws out-of-range exception
double oor = rating[(CardSuit)7]; // throws out-of-range exception

第一个泛型类型参数定义数组索引的类型;第二个参数定义数组元素类型。无参数构造函数创建一个所有元素都设置为其元素类型默认值的数组;带参数的构造函数将所有元素设置为相同的值。

上面的代码片段假设它与 3.2 中所示的 `CardSuit` 声明一起编译,该声明将其成员 `CardSuit.Length` 应用了 `NonEnumerable` 属性。在内部,`EnumerationIndexedArray` 类使用 `Enumeration`,因此 `NonEnumerable` 属性也适用于数组;因此,由 `CardSuit.Length` 值索引的数组元素不存在。对于这种情况,实现了最自然的行为:抛出 `IndexOutOfRangeException` 异常。如果索引是通过类型转换的整数值获得的,则尝试访问数组元素的结果取决于索引的值:如果该值不属于任何声明的枚举成员(未标记 `NonEnumerable` 属性),则会抛出相同的异常。

重要的是,分配给枚举成员的确切底层整数值并不重要。此类即使在两个或多个不同的枚举成员具有相同的底层整数值时也能正常工作。在这种情况下,可以通过这两个(或更多)枚举索引中的任何一个来访问相同的数组元素,无论使用哪个。

3.6 笛卡尔积

除了使用单个枚举类型索引的数组之外,我想考虑使用相同枚举类型的两个索引来索引的二维数组,它代表了基于由枚举值集表示的有限集的笛卡尔积

有人可能会问:为什么专门处理笛卡尔积索引,而不考虑具有不同索引类型组合的不同秩的数组?答案是:因为笛卡尔积在计算实践中非常重要。尽管枚举索引方法可以进一步泛化以涵盖更复杂的秩和索引类型的组合,但很难想象这项任务的复杂性可以得到很好的回报。同时,实现任何特定类型的数组结构,基于 `Enumeration` 功能,都没有问题。

至于笛卡尔积,它具有根本重要性,因为它可用于表示定义为笛卡尔积的二元关系子集:笛卡尔积上的`Boolean` 数组代表由枚举成员表示的集合元素之间的关系。

不同类型的笛卡尔积函数允许定义多重集和所有类型的有限,这些图在数学、计算机科学和通用计算任务中具有许多基本用途。也许最重要的应用之一将是实现有限状态机(FSM)。在我们的例子中,有限状态机可以建立在由枚举类型表示的状态集之上。

类 `CartesianSquareIndexedArray` 的工作方式与 `EnumerationIndexedArray` 非常相似:第一个泛型类型参数定义数组索引类型(假定为枚举类型),两者使用相同的类型;第二个参数定义笛卡尔积上的函数类型。笛卡尔积本身由枚举集与相同集合的笛卡尔积定义。请注意,如果第二个参数是 `Boolean`,那么这样的 `CartesianSquareIndexedArray` 可以被解释为关系,关系定义为笛卡尔积的子集,或者被解释为在节点集上定义的有向图,其中每个节点对应于单独的枚举成员,并且每个图边对应于数组值等于 `True` 的有序索引对。

现在,让我通过一个非常简单的有限状态机的例子来说明这个结构,例如表示某个线程的状态以及状态之间的转换图。严格来说,纯粹的数学有限状态机应使用 `Boolean` 数组元素类型,与上面解释的关系的笛卡尔表示完全相同,其中 `True` 值表示允许的转换,`False` 值表示禁止的(空的)转换。然而,在实践中,使用更高级的数组元素类型来携带每个转换的基本技术细节是有用的。在我的示例中,这是一个委托,它可以定义在每次转换时执行的某些操作。例如,该委托的一个实例可以调用物理机器的硬件控制,或提供转换的图形动画演示。

public enum State { Initial, Running, Paused, Aborted, Finished, }

public class EmptyTransitionException : System.ApplicationException {
    internal EmptyTransitionException(State from, State to)
        : base(FormatException(from, to)) {
        this.fFrom = from;
        this.fTo = to;
    } //EmptyTransitionException
    static string FormatException(State from, State to) {
        return string.Format(
            "State transition from {0} to {1} is not allowed",
            from, to); }
    public State From { get { return fFrom; } }
    public State To { get { return fTo; } }
    State fFrom, fTo;
} //class EmptyTransitionException

public delegate void StateTransition(State from, State to);

public abstract class StateMachine {

    public StateMachine() {
        StateGraph[State.Initial, State.Running] = Start;
        StateGraph[State.Running, State.Paused] = Pause;
        StateGraph[State.Paused, State.Running] = Resume;
        StateGraph[State.Running, State.Aborted] = Abort;
        StateGraph[State.Paused, State.Aborted] = Abort;
    } //StateMachine

    public void FireTransition(State from, State to) {
        StateTransition action = StateGraph[from, to];
        if (action == null)
            throw new EmptyTransitionException(from, to);
        else
            action(from, to);
    } //FireTransition

    protected abstract void Start(State from, State to);
    protected abstract void Stop(State from, State to);
    protected abstract void Pause(State from, State to);
    protected abstract void Resume(State from, State to);
    protected abstract void Abort(State from, State to);

    CartesianSquareIndexedArray<State, StateTransition>
        StateGraph =
            new CartesianSquareIndexedArray<State, StateTransition>();

} //class StateMachine

在此示例中,`StateGraph` 定义在由枚举类型 `State` 定义的节点集的笛卡尔积上。`CartesianSquareIndexedArray` 构建在此笛卡尔积之上,数组元素为委托类型。此数组表示节点之间的转换图,每个节点由单独的 `State` 成员表示。`abstract` 类 `StateMachine` 的目的是构建状态转换图的实例(仅为举例,它在构造函数中硬编码),并提供调用转换操作的间接层(上述示例中的一组 `abstract` 方法):每个操作通过 `FireTransition` 调用,期望两个 `State` 参数。这样,操作的实现可以与转换图无关,而 `StateMachine` 与转换操作无关。同样,这个例子是高度简化的。

4 工作原理

处理枚举类型的所有精细方面以及实现可枚举行为的有用思想已经在上面进行了描述。实现本身几乎没有什么特别之处。请查看源代码以了解所有细节。

在本节中,我将尝试概述解决方案的关键技术。

4.1 收集静态成员数据

可枚举行为基于 `ENUM` 类型 `public static` 字段的元数据,使用反射收集并处理成 `EnumerationItem<ENUM>` 实例数组(参见 3.1)。这个相对缓慢的过程不会在每次构造 `Enumeration` 时都重复。相反,当泛型类 `Enumeration<ENUM>` 使用新的 `ENUM` 类型实例化时,它会发生。这样,每个类型只有一个 `static` 部分已处理的 `ENUM` 元数据。不同的 `Enumeration` 实例共享此单例元数据,它包含 `EnumerationItem<ENUM>` 实例的数组以及一些额外的 `static` 成员,特别是用于线程安全(4.3)和数组索引(4.4)的成员。

我们需要的枚举成员是 `public static` 字段。这是如何获取它们

static FieldInfo[] GetStaticFields(Type type) {
    return type.GetFields(BindingFlags.Static | BindingFlags.Public);
} //GetStaticFields

记住,它适用于任何类型,无论是枚举还是非枚举。这两种情况通过 `EnumerationItem.EnumValue` 和 `EnumerationItem.Value` 的两个不同属性来区分,如 3.13.3 中所述。这是如何做的

static void BuildEnumerationCollectionCore() {
    Type type = typeof(ENUM);
    Bool isEnum = type.IsEnum;
    FieldInfo[] fields = GetStaticFields(type);
    List<EnumerationItem<ENUM>> list = new List<EnumerationItem<ENUM>>();
    Cardinal currentIndex = 0;
    for (Cardinal jj = 0; jj < (Cardinal)fields.Length; jj++) {
        FieldInfo field = fields[jj];
        object[] attributes =
            field.GetCustomAttributes(typeof(NonEnumerableAttribute), false);
        if (attributes.Length > 0) continue;
        object objValue = field.GetValue(null); //boxed if ENUM is primitive
        if (objValue == null) continue;
        ENUM enumValue = default(ENUM);
        if (isEnum)
            enumValue = (ENUM)objValue;
        else {
            if (objValue is ENUM)
                enumValue = (ENUM)objValue;
        } //if not enum
        string name = field.Name;
        list.Add(new EnumerationItem<ENUM>(name, currentIndex, objValue, enumValue));
        currentIndex++;
    } //loop
    EnumerationCollection = list.ToArray();
    FCollectionLength = (Cardinal)EnumerationCollection.Length;
} //BuildEnumerationCollectionCore

请注意,`isEnum` 在循环之前计算,当它为 `true` 时,当前成员类型的检查(相对较慢的方法)不会在循环中进行,因为枚举类型保证其所有字段都属于同一类型。

元数据单例基于惰性求值进行初始化。上述初始化是在给定 `ENUM` 类型第一次请求任何操作时执行的。由于 `Enumeration` 的某些操作是静态的(如 `CollectionLength`),这可能发生在调用构造函数之前。

4.2 枚举器

`Enumeration` 实例支持 `foreach` 语句,并通过实现 `IEnumerable` 接口(只有一个需要实现的方法:`IEnumerable.GetEnumerator`)表现为可枚举容器。

方法 `IEnumerable.GetEnumerator` 的实现返回内部类 `Enumerator` 的一个实例。它使用已处理的元数据单例实现 `IEnumerator` 接口。有一件重要的事情需要注意:每次 `foreach` 循环重新开始时,枚举器的当前位置都应该重置到初始位置(也取决于谓词 `IsReverse` 的当前值)。触发此重置的唯一位置是 `IEnumerable.GetEnumerator` 方法。

4.3 线程安全和性能

线程安全是可选的,由条件编译符号 `THREAD_SAFE_ENUMERATIONS` 定义。

即使在多线程应用程序中使用非线程安全版本代码也没有什么问题。

尽管 `Enumeration` 的静态元数据单例在可能属于不同线程的 `Enumeration` 不同实例之间共享,但对该单例的访问是只读的。锁定的唯一目标是保护该单例的初始化,该初始化很少并发进行。换句话说,如果应用程序是多线程的并使用非线程安全版本代码,它应该防止 `Enumeration` 元数据单例中的并发初始化。一种确保此规则得到遵守的简单方法是,在启动任何其他线程之前,至少为每个 `ENUM` 类型在主线程中执行一次 `Enumeration` 元数据单例的初始化。为了确保此初始化,只需调用 `Enumeration` 构造函数或读取其任何 `static` 属性,至少为每个要使用的 `ENUM` 类型调用一次。

也许上述细致的考虑是多余的:我的性能测试在仅测量迭代时间时没有显示任何可测量的线程安全时间损失。然而,线程安全使得重复的 `Enumeration` 构造函数调用速度慢了两倍多。(在所有测试中,第一次调用构造函数是在循环之前进行的,以单独测量元数据单例初始化;每个枚举成员的初始化速度比一次迭代(40 ns)慢 300-350 倍,但由于缓慢的基于反射的代码,锁定时间几乎不计入初始化时间:一个包含 1024 个成员的枚举类型的初始化需要 15-50 毫秒,这也足够好了,因为应用程序中枚举成员的总数通常要少得多。)对于几乎所有实际用途,使用线程安全的代码编译不会损害性能。

总之,对于使用线程安全或非线程安全版本代码编译的多线程应用程序,`Enumeration` 的实例不应在不同线程之间共享;否则,应用程序本身应负责同步。换句话说,无论编译条件如何,都不会使用锁定来保护 `Enumeration` 实例的并发使用。

4.4 数组索引

正如 3.5 中的示例所示,数组索引基于泛型类 `EnumerationIndexedArray<INDEX, ELEMENT>`。在内部,此类使用 `Enumeration<INDEX>` 类的 `static` 部分,该类提供了一个字典,用于根据 `INDEX` 类型的值快速查找 `EnumerationItem` 类实例在已处理元数据中的索引。此字典基于惰性求值创建和填充:即使已初始化相应 `Enumeration<INDEX>` 类型静态存储的已处理元数据,也不会创建字典,直到第一次尝试访问数组元素。

对于基于笛卡尔积的数组的索引,使用完全相同的技术;只有底层数组是二维的。这两个数组类都使用相同的方法来查找基于枚举值的整数数组索引。此方法 `GetIntegerIndexFromEnumValue(ENUM index)` 在 `Enumeration` 类内部实现。

5 代码构建和兼容性

代码适用于 Microsoft .NET 版本 2.0 到 4.0。解决方案*Enumerations.2005.sln*、*Enumerations.2008.sln* 和 *Enumerations.2010.sln* 允许使用相应版本的 Microsoft Visual Studio 构建代码。

可以使用批处理文件*build.2005.bat*(使用 Microsoft .NET 版本 2.0 的*MSBuild.exe* 进行构建,目标是同一版本)、*build.2008.bat* 和 *build.2010.bat*(使用 Microsoft .NET 版本 3.5 和 4.0 的*MSBuild.exe* 进行构建,目标是版本 3.5)以批处理模式构建代码。批处理构建不需要安装 Visual Studio 或任何其他开发环境 — 它仅使用 .NET 可再发行组件包。

5.1 兼容性:Microsoft .NET

Visual Studio 2008 和 2010 的项目版本是相互兼容的,即 Microsoft .NET 版本 3.5 的*MSBuild.exe* 可以构建*Enumerations.2008.sln*,反之,Microsoft .NET 版本 4.0 的*MSBuild.exe* 可以构建*Enumerations.2010.sln*。请编辑批处理文件中的*MSBuild.exe*路径以使用可用的 .NET 版本。

Visual Studio 2008 和 2010 的解决方案使用完全相同的源文件集,包括项目文件。然而,*Enumerations.2005.sln* 解决方案需要特殊的项目文件(后缀为“*.2005.csproj*”)。*Enumerations.2005.csproj* 项目的结构略有不同,因为它使用了主源文件的特殊版本,该文件链接自 *Enumerations.CLI2.0-compatible\Enumeration.cs*。我不得不创建一个该文件的特殊版本,因为 v. 2.0 必须使用 `System.Threading.ReaderWriterLock` 类而不是更高效的 `ReaderWriterLockSlim`(在 v. 3.5 和 4 中支持);这些类具有不同的接口。总之,出于 4.3 中解释的原因,线程安全以及 v. 2.0 和 v. 3.5 及更高版本之间线程安全版本的性能差异并不那么重要。

5.2 兼容性:Mono for Linux

为 .NET v.2.0 目标构建的所有软件均已成功测试与 Mono v. 1.2.6 和 v. 2.4.4 的二进制兼容性;为 .NET v.3.5 目标构建的软件测试证实了与 Mono v. 2.4.4 的二进制兼容性。我在 Ubuntu Linux v. 8.04 上进行了所有测试。

6 结论

对枚举类型的枚举支持不足 — 以 `IEnumerable` 接口支持或其他形式 — 是 .NET 和 C# 中一种相当陈旧的功能,因此它甚至不能证明“枚举”一词的合理性。至少有三种方法可以克服这个问题:扩展 .NET CLI、扩展一个或多个 CLI 语言或将预定义数据类型封装在实现所需功能的类中。

在实践中,只有第三种方法在我能力范围内;因此,本工作是成功实现预期效果的尝试。

© . All rights reserved.