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

符号作为可扩展枚举

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.43/5 (9投票s)

2009 年 4 月 1 日

MIT

8分钟阅读

viewsIcon

106083

downloadIcon

327

使用 Symbol 类来定义可由其他类扩展的枚举类值。

  • 下载源代码 - 4.92 KB
  • 注意:此代码包含 NUnit 的单元测试,但如果您没有 NUnit,可以轻松删除这些测试。

引言

在 C# 中,有时您会想定义一个基类或库,它使用枚举,并且您希望允许派生类或库的用户为它定义新值。问题是,enum 不可扩展:派生类或用户代码无法定义新值。

例如,有一次,我编写了一个用于将具有地理坐标的“形状”序列化和反序列化到文本文件的库。在这个库中,支持多种不同的形状:圆形、矩形、线条、多边形等。假设有一个 `ShapeType` 枚举用于此目的

public enum ShapeType {
    Circle, 
    Rect,
    Line,
    Polygon
}

枚举非常适合存储在文本文件中,因为您可以写入 `t.ToString()` 将 `ShapeType t` 转换为字符串,并使用 `Enum.Parse(typeof(ShapeType), s) 将字符串 `s` 转换回 `ShapeType`。

但是,如果您想允许其他开发人员定义自己的形状,该怎么办?其他开发人员无法向 `ShapeType` 添加新值,即使他们可以,也存在两个开发人员将相同整数值分配给不同种类形状的风险。我们如何解决这些问题?

Ruby 来帮忙 

有时,当需要可扩展枚举时,人们会使用字符串或整数常量(const intreadonly int)而不是枚举。这些解决方案至少存在以下问题: 

  • 字符串和整数通常不用于枚举。因此,当其他开发人员看到一个“string”或“int”属性或参数时,他们不会立即意识到它用于枚举。 
  • 由于可以错误地输入字符串或意外地将字符串/整数放入本应是枚举值(反之亦然)的位置,因此会丢失一些静态类型的好处。
  • 字符串无法使用重构工具(如 Visual Studio 的“重命名”功能)进行重命名。
  • 与枚举相比,字符串在相等性比较时速度较慢,并且与用作字典键的整数相比速度也较慢(尽管由于 Microsoft 的一个奇怪决定,枚举作为字典键的性能也很差)。
  • 使用整数时,很难保证两个不同的开发人员在扩展枚举时都使用唯一值。

在动态语言 Ruby 中,我们通常使用 *符号(symbols)* 而不是枚举。符号类似于字符串文字,但不是像 "Circle" 这样的字符串,而是使用符号语法 :Circle

在大多数情况下,符号可以解决上述问题。它们的比较速度与整数一样快,而且不会与字符串混淆。由于任何人都可以随时定义新符号,因此符号就像一个大小无限的枚举。而且,如果您按照我下面推荐的方式使用它们,就可以使用重构工具重命名它们。 

(编辑:后来我发现其他语言也有内建的符号概念,例如 LISP)  

中的符号 

我为 .NET 编写了一个 `Symbol` 实现,您可以将其用作可扩展枚举。我现在将演示如何使用 `Symbol` 来重写我们的 `ShapeType` 枚举。首先,将 `enum ShapeType 更改为一个类。然后,用 `Symbol` 替换每个枚举值

public static class ShapeType
{
    public static readonly Symbol Circle  = GSymbol.Get("Circle");
    public static readonly Symbol Rect    = GSymbol.Get("Rect");
    public static readonly Symbol Line    = GSymbol.Get("Line");
    public static readonly Symbol Polygon = GSymbol.Get("Polygon");
}

如果第三方想扩展此符号列表,他们应该编写另一个静态类来添加其他选项。例如,Xyz 公司可能会编写此扩展

public static class FractalShape
{
    public static readonly Symbol Mandelbrot = 
                  GSymbol.Get("XyzCorp.Mandelbrot");
    public static readonly Symbol Julia = GSymbol.Get("XyzCorp.Julia");
    public static readonly Symbol Fern = GSymbol.Get("XyzCorp.Fern");
}

为了确保两个独立方不会意外地为两种不同的形状定义相同的符号(例如,如果两个不同方都创建了一个名为 `Fern` 的形状),建议在调用 `GSymbol.Get` 时尝试使用唯一的名称。因为 `GSymbol.Get` 在给出相同输入字符串时总是返回相同的 `Symbol`。因此,在此示例中,我使用了前缀“XyzCorp.”来确保 Xyz 公司定义的名称是唯一的。 

类型安全的符号 

当我第一次写这篇文章时,人们抱怨 `Symbol` 不是类型安全的:您可能会不小心混淆两个不相关的枚举,因为它们都具有 `Symbol` 类型。而且,根据 3 票的投票,我的文章被投入了零读者群的深渊。此外,如上定义的 `ShapeType` 并非其 enum 等价物的即插即用替换,因为 `ShapeType` 变量声明必须更改。

ShapeType rect = ShapeType.Rect;

将被更改为此

Symbol rect = ShapeType.Rect; 

现在您可以使用类型安全的“符号池”来克服这些限制。`SymbolPool` 是符号的“命名空间”。有一个永久的全局池(由 `GSymbol.Get` 使用),您可以创建无限数量的私有池。我将在下面详细介绍它们的工作原理,但现在,让我向您展示如何使用 `SymbolPool<ShapeType>` 创建类型安全的、可扩展的枚举

public class ShapeType : Symbol
{
    private ShapeType(Symbol prototype) : base(prototype) { }
    public static new readonly SymbolPool<ShapeType> Pool 
                         = new SymbolPool<ShapeType>(p => new ShapeType(p));

    public static readonly ShapeType Circle  = Pool.Get("Circle");
    public static readonly ShapeType Rect    = Pool.Get("Rect");
    public static readonly ShapeType Line    = Pool.Get("Line");
    public static readonly ShapeType Polygon = Pool.Get("Polygon");
}

由于 `ShapeType` 的构造函数是 private,因此创建新的 `ShapeType` 的唯一方法是调用 `ShapeType.Pool.Get()`。

现在,第三方“XyzCorp”可以如下定义新的 `ShapeType`:

public class FractalShape : ShapeType
{
    public static readonly ShapeType Mandelbrot = 
                  Pool.Get("XyzCorp.Mandelbrot");
    public static readonly ShapeType Julia = Pool.Get("XyzCorp.Julia");
    public static readonly ShapeType Fern = Pool.Get("XyzCorp.Fern");
}

请注意,`FractalShape` 的成员仍然具有 `ShapeType` 类型。不必派生 `FractalShape` 自 `ShapeType`;我这样做只是为了清楚地表明两者是相关的。

使用符号 

  • 要将 `Symbol s` 转换为字符串,请调用 `s.Name`。您也可以调用 `s.ToString()`,但这会在名称前加上冒号(:),如 Ruby 中一样(编辑:我在较新版本的 `Symbol` 中删除了冒号,因此 `Symbol` 的行为更像普通的 `string 和 `enum。) 
  • 当您想将 string 转换回 `Symbol` 时,可以使用 `GSymbol.Get(string)` 创建全局符号,或者使用 `Pool.Get(string)`(其中 `Pool` 是私有符号池)来代替 `Enum.Parse。 
  • 要获取池中的所有符号,只需遍历该池即可: 
  • foreach (ShapeType s in ShapeType.Pool)
        ...

    符号的返回顺序与其创建顺序相同。

请注意,您创建的每个 `Symbol` 都会占用少量内存,如果 `Symbol` 的池存储在全局变量中,则该内存无法被垃圾回收。因此,如果字符串来自一个大文件,您可能希望调用 `Pool.GetIfExists(string)(其中 `Pool` 是私有池或 `GSymbol`)而不是(以避免内存泄漏)。`GetIfExists` 不会创建新符号,它只返回已存在的符号。因此,如果您得到一个无意义的名称,如“fdjlas”,`GetIfExists` 将返回 null 而不是一个有效的 `Symbol。 

有一个技巧:如果您使用 `GetIfExists`,您需要确保所有所需的符号都已存在。因此,在调用 `ShapeType.Pool.GetIfExists` 来解码形状类型名称之前,必须确保已初始化派生类型(如 `FractalShape`)。访问 `FractalShape` 中的任何 `ShapeType` 即可达到目的

// Returns null if FractalShape has never been used
ShapeType s = ShapeType.Pool.GetIfExists("XyzCorp.Fern");

s = FractalShape.Julia;
s = ShapeType.Pool.GetIfExists("XyzCorp.Fern"); // guaranteed to work 

符号如何工作,简而言之

此库有四个类。

  1. `Symbol` 只是一个小型类,具有只读的 `Name`、整数 `Id` 以及指向其 `Pool` 的引用。每个 `Symbol` 都存储在一个 `SymbolPool` 中。
  2. `SymbolPool` 包含一组 `Symbol`。`SymbolPool` 包含一个 `List<Symbol>` 和一个 Dictionary<string, Symbol>,用于分别按 ID 和名称查找符号。`SymbolPool` 是线程安全的;您可以安全地从不同线程在同一池中创建 `Symbol`。
  3. `SymbolPool<T>` 是 `SymbolPool` 的派生类,它创建 `T`,其中 `T` 是 `Symbol` 的派生类。您将一个工厂函数传递给其构造函数,当有人调用 `Get()` 创建 `T` 时,`SymbolPool` 会调用您的工厂函数,并传递一个“原型”作为参数(“原型”是您可以用来构建 `T` 的 `Symbol`)。
  4. `GSymbol` 包含“全局”`SymbolPool`。调用 `GSymbol.Get` 来创建“全局”`Symbol`。

每个 `Symbol` 都有一个 ID 号;这不过是一个计数器的值,每次创建符号时该计数器都会递增。ID 在给定池内是唯一的,但在不同池之间可能重复。私有池默认具有正 ID,从 1 开始;全局池具有负 ID,从 -1 开始,但 `GSymbol.Empty` 除外,它是表示空字符串(Name == "")的 `Symbol`。

因为它返回 ID 号而不是获取字符串的哈希码,所以 `GetHashCode()` 速度很快;因此,`Symbol` 用作 `Dictionary` 键时速度很快。比较 `Symbol` 的相等性速度很快,因为只比较引用,而不比较每个 `Symbol` 的内容。两个 `Symbol` 相等当且仅当它们位于同一内存位置。

除了创建类型安全的、可扩展的枚举之外,使用 `SymbolPool` 的另一个原因是可以构建一个临时的 `Symbol` 集合,该集合稍后可以被垃圾回收。当不再有指向池本身或其任何 `Symbol` 的引用时,`SymbolPool` 及其包含的所有 `Symbol` 都可以被垃圾回收。请注意,`Symbol` 对其池有引用,因此任何对 `Symbol` 的剩余引用都会使其整个池保持活动状态。 

至于我…… 

Loyc 编译器工具项目中,源代码用 Loyc trees 表示。在 Loyc trees 中,我使用 `Symbol` 而不是 `string` 来表示源代码中的所有标识符(变量名和方法名)以及内建运算符和构造函数的名称。这避免了存储字符串的多个副本并允许快速的相等性比较。 

历史 

  • 2008 年 6 月 1 日:第一个版本。
  • 2009 年 12 月 12 日:引入 SymbolPool。
  • 2009 年 12 月 14 日:在 CodeProject 发布。 
  • 2010 年 2 月 24 日:增加了对类型安全符号的支持。  
  • 2014 年 2 月 25 日:更正了格式错误。根据更新的信息编辑了一些文本。 
© . All rights reserved.