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

完美的 C# 字符串枚举器

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.76/5 (14投票s)

2007 年 12 月 31 日

CPOL

6分钟阅读

viewsIcon

70719

downloadIcon

176

一种在 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`(例如,“Ford”、“Toyota”等)。

实际的枚举器类

枚举器必须继承自 `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()` 提取文本值的依赖。
© . All rights reserved.