完美的 C# 字符串枚举器






2.76/5 (14投票s)
一种在 C# 中实现字符串枚举器的出色且简便的方法。
引言
我非常喜欢 C#,并尽我所能地后悔没有早点开始学习它。尽管如此,我仍然非常怀念其他语言中的一些东西。
以字符串枚举器为例。枚举器在保持代码简洁易维护方面非常出色。你定义一次,为其分配值,然后在整个程序中使用它们的名称。在后台,每个枚举都会被转换为你设置的值,最终存储在磁盘上,用于计算等。
字符串枚举也一样。你选择一组名称,为它们分配文本值,在代码中,你始终引用这些名称。当需要将其用于磁盘存储、显示等时,你引用枚举的值。
但在 C# 中,出于某种原因(有人知道为什么吗?),这一点被省略了。我寻找了很长时间,找到了各种解决方案,从简单地创建一个带有静态字符串的类到使用反射。它们要么太简单,要么太复杂,没有一个提供了枚举器应有的所有功能。
于是,我开始着手自己做一个,我认为我已经做到了。我认为它 100% 模仿了常规枚举器,并提供了与常规枚举器完全相同的功能、用法和行为。我想不到还有什么遗漏的,并希望读者能帮助指出那些不足之处 :)
关于下载的一个快速说明 - 其中包含一个用于测试枚举器的 NUnit 项目。如果你没有 NUnit,只需从解决方案中删除该项目即可。如果你有 NUnit 并想运行测试,请将测试项目中的 `nunit.framework` 引用重新指向你本地的 *nunit.framework.dll* 副本,重新生成解决方案,运行 NUnit,然后加载包含的 *StringEnumerator.nunit* 项目文件。
背景
当我开始创建这个枚举器时,我不得不坐下来思考枚举器可以使用的所有方式,以确保字符串枚举器尽可能地与其名称相符。我使用 NUnit 创建了许多测试,到目前为止一切顺利。我真的想不出更多好的测试了,但如果其他人能想到,请告诉我。
用法
你可以参考 NUnit 项目中的测试来了解枚举器的功能,但这里简要介绍一下:
- 将静态枚举器的值赋给一个字符串。
- 实例化一个枚举器对象,将其设置为一个有效的枚举值,将其作为参数传递给一个方法,并将其值赋给一个字符串。
- 实例化一个枚举器对象,将其设置为一个有效的枚举值,将其作为参数传递给一个方法,将该枚举器复制到新的枚举器,并将新枚举器的值赋给一个字符串。
- 与上面相同,但在将第一个枚举器复制到第二个枚举器后,我们更改第一个枚举器的值,并确保第二个枚举器不受影响。请注意,尽管我们处理的是一个引用项,但枚举器类是不可变的,因此在为其赋新值时,我们总是创建一个新的副本。将一个现有对象赋给另一个对象只需使用 "=" 符号即可,不依赖于 `ICloneable` 接口,并且复制不是浅拷贝(尽管这个类本身并不深)。
- 测试包含空格的枚举字符串。
- 将一个有效的字符串值赋给一个枚举器对象。
- 将一个无效的字符串值赋给一个枚举器对象,并引发异常。
- 创建两个枚举器对象,并确保 `==`、`!=` 和 `Equals()` 方法运行正常。
一旦你创建了枚举器类,它的使用方式就和使用常规枚举器完全一样,只有一个细微的区别:当你想要处理枚举器对象或静态枚举值之一的实际字符串值时,你必须使用 `ToString()` 方法。我非常希望能找到绕过这一点的方法,但即使找不到,我也认为这是一个很小的代价。
EStringEnum 基类
要创建字符串枚举器,你需要两样东西:基类 `EStringEnum`,以及你定义的类(继承自 `EStringEnum`)以及实际的字符串值。
基类暴露了两个必须重写的项,以及用于验证用户尝试赋给枚举器对象(即,值必须是有效的枚举字符串之一)的字符串值的逻辑,以及一些 `ToString()` 和各种运算符的重载。
/// <summary>
/// A string enumerator base class. Must be inherited.
/// </summary>
public abstract class EStringEnum
{
#region Data
protected string mCurrentEnumValue = "";
protected abstract string EnumName { get; }
protected abstract List<string> PossibleEnumValues { get; }
#endregion
#region Constructors
public EStringEnum() { }
/// <summary>
/// A string enumerator
/// </summary>
/// <param name="value">A valid enumerator value. An exception is raised
/// if the value is invalid.</param>
public EStringEnum(string value)
{
if (PossibleEnumValues.Contains(value))
{
mCurrentEnumValue = value;
}
else
{
string errorMessage = string.Format(
"{0} is an invalid {1} enumerator value",
value,
EnumName);
throw new Exception(errorMessage);
}
}
#endregion
#region Overloads
/// <summary>
/// Returns the enumerator's current value
/// </summary>
/// <returns></returns>
public override string ToString()
{
return mCurrentEnumValue;
}
/// <summary>
/// Test for equality
/// </summary>
/// <param name="stringEnum1"></param>
/// <param name="stringEnum2"></param>
/// <returns></returns>
public static bool operator ==(EStringEnum stringEnum1, EStringEnum stringEnum2)
{
return (stringEnum1.ToString().Equals(stringEnum2.ToString()));
}
/// <summary>
/// Test for inequality
/// </summary>
/// <param name="stringEnum1"></param>
/// <param name="stringEnum2"></param>
/// <returns></returns>
public static bool operator !=(EStringEnum stringEnum1, EStringEnum stringEnum2)
{
return (!stringEnum1.ToString().Equals(stringEnum2.ToString()));
}
/// <summary>
/// Test for equality
/// </summary>
/// <param name="o"></param>
/// <returns></returns>
public override bool Equals (object o)
{
EStringEnum stringEnum = o as EStringEnum;
return (this.ToString().Equals(stringEnum.ToString()));
}
/// <summary>
/// Retrieve the hashcode
/// </summary>
/// <returns></returns>
public override int GetHashCode()
{
return base.GetHashCode();
}
#endregion
}
继承此基类时,只需实现两个项。
protected abstract string EnumName { get; }
protected abstract List<string> PossibleEnumValues { get; }
`EnumName` 必须返回一个字符串,其中包含字符串枚举器的名称(例如,`ECarManufacturers`),而 `PossibleEnumValues` 必须返回一个包含枚举字符串的 `List
实际的枚举器类
枚举器必须继承自 `EStringEnum`,并执行以下操作:
- 实现上面提到的两个属性,
- 为每个枚举字符串提供静态访问器,
- 提供一个构造函数(该构造函数只调用基类构造函数),以及
- 提供从 `string` 类型到实际枚举器(例如 `ECarManufacturers`)的 `object` 类型的隐式转换。
这听起来可能很复杂,但正如下面的示例所示,它的实现确实非常简单。
/// <summary>
/// A car manufacturer enumerator
/// </summary>
public class ECarManufacturers: EStringEnum
{
#region Data
/// <summary>
/// Used by EStringEnum to identify the current class
/// </summary>
protected override string EnumName { get { return "ECarManufacturers"; } }
protected override List<string> PossibleEnumValues
{
get { return mPossibleEnumValues; }
}
/// <summary>
/// Complete list of string values that this enumerator can hold
/// </summary>
private static List<string> mPossibleEnumValues = new List<string>()
{
"Toyota",
"Honda",
"Ford",
"Chrysler",
"Volvo",
"General Motors"
};
/// <summary>
/// CarManufacturers type
/// </summary>
static public ECarManufacturers Toyota
{
get { return new ECarManufacturers(mPossibleEnumValues[0]); }
}
/// <summary>
/// CarManufacturers type
/// </summary>
static public ECarManufacturers Honda
{
get { return new ECarManufacturers(mPossibleEnumValues[1]); }
}
/// <summary>
/// CarManufacturers type
/// </summary>
static public ECarManufacturers Ford
{
get { return new ECarManufacturers(mPossibleEnumValues[2]); }
}
/// <summary>
/// CarManufacturers type
/// </summary>
static public ECarManufacturers Chrysler
{
get { return new ECarManufacturers(mPossibleEnumValues[3]); }
}
/// <summary>
/// CarManufacturers type
/// </summary>
static public ECarManufacturers Volvo
{
get { return new ECarManufacturers(mPossibleEnumValues[4]); }
}
/// <summary>
/// CarManufacturers type
/// </summary>
static public ECarManufacturers GeneralMotors
{
get { return new ECarManufacturers(mPossibleEnumValues[5]); }
}
#endregion
#region Constructor
/// <summary>
/// A car manufacturer enumerator
/// </summary>
/// <param name="value">A valid enumerator value.
/// An exception is raised if the value is invalid.</param>
private ECarManufacturers(string value): base(value)
{ }
#endregion
#region Misc methods
/// <summary>
/// Implicitly convert a string to a CarManufacturers object
/// </summary>
/// <param name="value">A string value to convert to an ECarManufacturers
/// enum value. An exception is raised if the value is invalid.</param>
/// <returns></returns>
public static implicit operator ECarManufacturers(string value)
{
return new ECarManufacturers(value);
}
/// <summary>
/// Implicitly convert an ECarManufacturers object to a string
/// </summary>
/// <param name="carManufacturers">A ECarManufacturers object
/// whose value is to be returned as a string
/// <returns></returns>
public static implicit operator string(ECarManufacturers carManufacturers)
{
return carManufacturers.ToString(););
}
#endregion
}
示例
NUnit 测试项目中包含以下测试(可在下载中找到),但我在这里也发布它们,以便你能快速了解此实现的真实性。
[TestFixture]
public class TestClass1
{
[Test]
public void Test01()
{
string result = ECarManufacturers.GeneralMotors.ToString();
string expected = "General Motors";
Assert.IsTrue(result == expected);
}
[Test]
public void Test02()
{
ECarManufacturers carManufacturer = ECarManufacturers.Honda;
string result = carManufacturer.ToString();
string expected = "Honda";
Assert.IsTrue(result == expected);
}
[Test]
public void Test03()
{
Test03A(ECarManufacturers.Ford);
}
private void Test03A(ECarManufacturers carManufacturer)
{
string expected = "Ford";
string result = carManufacturer.ToString();
Assert.IsTrue(result == expected);
}
[Test]
public void Test04()
{
Test04A(ECarManufacturers.Ford);
}
private void Test04A(ECarManufacturers carManufacturer)
{
ECarManufacturers tempCarManufacturers2 = carManufacturer;
string result = tempCarManufacturers2.ToString();
string expected = "Ford";
Assert.IsTrue(result == expected);
}
[Test]
public void Test05()
{
Test05A(ECarManufacturers.Ford);
}
private void Test05A(ECarManufacturers carManufacturer)
{
ECarManufacturers carManufacturer2 = carManufacturer;
carManufacturer = ECarManufacturers.Chrysler;
string expected1 = "Chrysler";
string result1 = carManufacturer.ToString();
string expected2 = "Ford";
string result2 = carManufacturer2.ToString();
Assert.IsTrue(result1 == expected1 && result2 == expected2);
}
[Test]
public void Test06()
{
ECarManufacturers tempCarManufacturers = "Ford";
string result = tempCarManufacturers.ToString();
string expected = "Ford";
Assert.IsTrue(result == expected);
}
[Test]
public void Test07()
{
ECarManufacturers tempCarManufacturers2 = null;
try
{
tempCarManufacturers2 = "Orion";
Assert.Fail();
}
catch (Exception ex)
{
Assert.IsTrue(ex.Message.Equals(
"Orion is an invalid ECarManufacturers enumerator value"));
}
}
[Test]
public void Test08()
{
ECarManufacturers tempCarManufacturers = "General Motors";
Assert.IsTrue(tempCarManufacturers.ToString().Equals(
ECarManufacturers.GeneralMotors.ToString()));
}
[Test]
public void Test09()
{
ECarManufacturers carManufacturers1 = ECarManufacturers.GeneralMotors;
ECarManufacturers carManufacturers2 = ECarManufacturers.GeneralMotors;
Assert.IsTrue(carManufacturers1 == carManufacturers2);
}
[Test]
public void Test10()
{
ECarManufacturers carManufacturers1 = ECarManufacturers.GeneralMotors;
ECarManufacturers carManufacturers2 = ECarManufacturers.Ford;
Assert.IsTrue(carManufacturers1 != carManufacturers2);
}
[Test]
public void Test11()
{
ECarManufacturers carManufacturers1 = ECarManufacturers.GeneralMotors;
int number = carManufacturers1.GetHashCode();
}
}
结论
考虑到它,这并不是一个非常复杂的解决方案,而且我认为它涵盖了枚举器应有的所有行为。当需要使用枚举器时,其使用方式与使用常规枚举器完全相同。如果你能想到任何我遗漏或可以改进的地方,请随时告诉我。
我想改进的一个方面
- 将尽可能多的逻辑提取出来并放入基类(例如静态访问器)。我可以使用 Spring.Net 之类的东西在运行时注入访问器,但这有两个主要缺点:Intellisense 将不再起作用,并且我会为项目添加一个依赖项。
历史
- 2007 年 12 月 31 日 - 初次发帖(新年快乐!)。
- 2008 年 1 月 1 日 - 添加了示例。
- 2008 年 1 月 2 日 - 移除了从 `ToString()` 提取文本值的依赖。