StringEnum 模拟 System.Enum 的工作方式
一个基类,用于密切模拟具有字符串底层类型的枚举行为。
引言
如果您曾经维护、使用和比较过一串字符串常量,您可能已经遇到过这样的挫败感:即使在 .NET 2.0 中,我们也还没有原生的字符串枚举。此外,您无法继承 System.Enum
类,因此无法扩展它来支持字符串。
一个 StringEnum
在定义可能的数据库值、保存用于填充列表的字符串值的可枚举列表等方面将非常有用。已经创建了许多变通方法,但它们都有各自的问题。
只是一个警告:实现可能有点粗糙,因为我只是想尽快发布一些内容供人们查看和开始使用。话虽如此,欢迎反馈。
背景
我见过几种不同的方法来实现类似真正的字符串枚举的功能。它们如下:
Enum.ToString
- 那么为什么不直接使用一个简单的Enum
,然后只使用ToString
和Parse
函数来相互转换字符串呢?- 优点包括易于定义,以及大多数常规 Enum 的优点。
- 主要缺点是名称严格绑定到其值。像 "
PF_SCR_ATTRIB_WIDESCREEN
" 这样的名称被不必要地装饰,而 "SA" 被不必要地缩写。它们可以更好地表达为 "Widescreen" 或 "Sales"。 (我知道 XML 注释可以缓解,但我们正在努力实现清晰、简洁的代码。) 此外,每次想处理字符串值时,都必须使用ToString
和Parse
函数。此外,所有排序都按数字值进行,并且枚举类无法扩展。 - Attributes (属性) - 在此方法中,定义了一些自定义属性并将其附加到常规的
Enum
。 (一个很好的例子可以在 这里找到。) - 由于它是一个
Enum
,因此优点是Enum
的所有优点——可以获取值的列表,可以从字符串解析名称,等等。 - 缺点是必须显式调用一个函数来“挖掘”属性以解析值或返回一个值,而且由于该函数是通用的,因此在手动输入函数名称、然后输入枚举名称和 "." 之前,您不会获得 IntelliSense 的任何好处。此外,基类型仍然是数字,因此赋值和比较仍然在数字级别上进行。
- Constants (常量) - 使用一些鲜为人知的技术(在
StringEnum
代码中使用),在一个类上定义一组常量实际上是一个相当不错的方法。 - 优点是,由于您的枚举值都是字符串常量,因此所有比较都是字符串比较。
- 缺点是字符串比较只是普通的字符串比较,依赖于
Option Compare
或大量的StrComp
来区分大小写。此外,没有类型检查,并且不允许无名称值。 - Structure (结构) - 结构的一个例子可以在 这里找到。
- 这是最好的方法之一,因为结构的行为像值类型,这使得概念上易于跟踪。可以重载运算符,并且您可以实现自己的函数来使用该结构。
- 然而,对于结构,没有继承,并且所有扩展字符串枚举的代码都必须四处复制——如果您重载的不仅仅是
Equals
函数,则很困难。另一个问题是,结构实际上不是必需的,因为您最终所做的只是复制对ReadOnly
字符串值的引用。不如直接传递一个类的引用。
Using the Code
StringEnumBase
提供了一个可继承的基类,它提供了与 System.Enum
类相当的功能。
它具有以下优点:
- 类型检查。
- 比较发生时 IntelliSense 弹出。
- 自定义鼠标悬停,显示名称和值。
- 使用基本运算符进行受控比较。默认不区分大小写。可以通过将提供的属性分配给继承类来区分大小写。
- 可以接受名称值以外的值。可以通过将提供的属性分配给继承类来仅限于名称值。
- 可以使用
System.ComponentModel.Description
为每个命名值分配描述,并通过基类的.ToDesc
函数获取它。 - 提供
System.Enum
类所有其他相关函数(例如GetValues
、Parse
、CompareTo
、GetNames
等)。
以以下方式继承它,将继承者的类型名称作为泛型类型。这样基类就可以获取继承类的类型——用于获取属性、字段等。您还必须声明参数化构造函数,以便它可以保持私有和参数化。
添加 completionlist
XML 注释以获取 IntelliSense 弹出。 StringEnumRegisteredOnly
属性是用于修改 StringEnumBase
行为的可选属性之一。
''' <completionlist cref="Numbers" />
<StringEnumRegisteredOnly()> _
Public Class Numbers
Inherits StringEnumBase(Of Numbers)
Private Sub New(ByVal StrValue As String)
MyBase.New(StrValue)
End Sub
...
然后,您可以将值枚举为 Shared ReadOnly
字段或属性。我更喜欢使用字段,因为它占一行。您也可以在声明中使用 DescriptionAttribute
。
<Description("This is test value one.")> _
Public Shared ReadOnly One As New Numbers("ONE")
<Description("This is test value two.")> _
Public Shared ReadOnly Property Three() As Numbers
Get
Return New Numbers("TWO")
End Get
End Property
关注点
看起来 .NET 的一些部分仍然很粗糙,即使经过这么多年——尤其是在 VB.NET 中。例如,在 DebuggerDisplayAttribute
中,后缀 "nq" 被记录为会去除其后缀值中的引号。唯一的问题是它只适用于 C#。看起来内联条件也只适用于 C#。
我还偶然发现了 completionlist
XML 注释。为什么这个功能如此方便却没有任何文档?
历史
- 2007-05-02 - 初始发布。
- 2007-05-22 - 修改以更好地符合预期行为并添加一些功能。
- 添加了更多比较运算符函数,以处理
StringEnum
直接与字符串或其他可以转换为字符串的对象(包括不同类型的StringEnum
)进行比较的情况。这允许与字符串进行比较,而无需将字符串转换为StringEnum
类型,如果StringEnum
设置为仅接受已注册的值,这可能会导致错误。 - 添加了向字符串的缩小转换。声明为缩小转换很重要,因为其他对象将在
StringEnum
转换为字符串之前转换为StringEnum
,这使我们能够在某些情况下控制大小写敏感性。 - 实现了
IConvertible
接口,以便StringEnum
将自动转换为字符串;例如,当它传递给一个期望字符串参数的函数时。这一点很重要,因为CType
并非在所有情况下都会自动调用。 - 修改了
GetValues()
函数以返回一个类型化数组而不是泛型System.Array
。 - 添加了用于解析描述以获取
StringEnum
值的函数。 - 添加了共享属性,用于返回特定的
StringEnum
是否正在使用行为修改属性。