枚举的字符串表示

955
我有以下的枚举:
public enum AuthenticationMethod
{
    FORMS = 1,
    WINDOWSAUTHENTICATION = 2,
    SINGLESIGNON = 3
}

问题是,当我要求 AuthenticationMethod.FORMS 时,我需要单词 "FORMS" 而不是 id 1。
我已经找到了解决这个问题的方法(link):
首先,我需要创建一个名为 "StringValue" 的自定义属性:
public class StringValue : System.Attribute
{
    private readonly string _value;

    public StringValue(string value)
    {
        _value = value;
    }

    public string Value
    {
        get { return _value; }
    }

}

然后我可以将这个属性添加到我的枚举中:
public enum AuthenticationMethod
{
    [StringValue("FORMS")]
    FORMS = 1,
    [StringValue("WINDOWS")]
    WINDOWSAUTHENTICATION = 2,
    [StringValue("SSO")]
    SINGLESIGNON = 3
}

当然,我需要一些东西来获取那个StringValue。
public static class StringEnum
{
    public static string GetStringValue(Enum value)
    {
        string output = null;
        Type type = value.GetType();

        //Check first in our cached results...

        //Look for our 'StringValueAttribute' 

        //in the field's custom attributes

        FieldInfo fi = type.GetField(value.ToString());
        StringValue[] attrs =
           fi.GetCustomAttributes(typeof(StringValue),
                                   false) as StringValue[];
        if (attrs.Length > 0)
        {
            output = attrs[0].Value;
        }

        return output;
    }
}

好的,现在我有了获取枚举的字符串值的工具。 然后我可以像这样使用它:
string valueOfAuthenticationMethod = StringEnum.GetStringValue(AuthenticationMethod.FORMS);

好的,现在所有这些都能正常工作,但我觉得这是一项很繁琐的工作。我想知道是否有更好的解决方案。
我还尝试过使用字典和静态属性,但那也不是更好的解决方案。

5
虽然这段话有点啰嗦,但其实是一种非常灵活的处理方式。正如我的一位同事所指出的,这种方法可以用于许多情况,以取代将数据库代码映射到枚举值等的“Enum Helpers”。 - BenAlabaster
3
这是一个“枚举”,而不是一个“枚举器”。 - Ed S.
29
MSDN建议在属性类名后面加上"Attribute"后缀,因此建议将"class StringValueAttribute"改为 "StringValueAttribute类"。 - serhio
14
我同意@BenAlabaster的观点,这实际上相当灵活。此外,您可以通过在静态方法中在“Enum”前面添加“this”,将其变成扩展方法。然后,您可以执行“AuthenticationMethod.Forms.GetStringValue();”。 - Justin Pihony
5
这种方法使用反射来读取属性值,如果你需要多次调用GetStringValue(),据我的经验,它非常缓慢。类型安全枚举模式更快。 - Rn222
显示剩余2条评论
37个回答

888

尝试使用类型安全的枚举模式。

public sealed class AuthenticationMethod {

    private readonly String name;
    private readonly int value;

    public static readonly AuthenticationMethod FORMS = new AuthenticationMethod (1, "FORMS");
    public static readonly AuthenticationMethod WINDOWSAUTHENTICATION = new AuthenticationMethod (2, "WINDOWS");
    public static readonly AuthenticationMethod SINGLESIGNON = new AuthenticationMethod (3, "SSN");        

    private AuthenticationMethod(int value, String name){
        this.name = name;
        this.value = value;
    }

    public override String ToString(){
        return name;
    }

}
更新 可以通过以下方式进行显式(或隐式)类型转换:
  • 添加具有映射的静态字段

  • private static readonly Dictionary<string, AuthenticationMethod> instance = new Dictionary<string,AuthenticationMethod>();
    
    • 请注意:为了避免在调用实例构造函数时因初始化“枚举成员”字段而引发NullReferenceException,请确保将字典字段放置在类中的“枚举成员”字段之前。这是因为静态字段初始化器按声明顺序调用,并且在静态构造函数之前调用,从而创建了奇怪但必要且令人困惑的情况:在所有静态字段被初始化以及静态构造函数被调用之前,实例构造函数可能会被调用。
    • 在实例构造函数中填充该映射

    instance[name] = this;
    
  • 并添加用户定义类型转换运算符

  • public static explicit operator AuthenticationMethod(string str)
    {
        AuthenticationMethod result;
        if (instance.TryGetValue(str, out result))
            return result;
        else
            throw new InvalidCastException();
    }
    

23
它看起来像一个枚举,但它并不是一个枚举。如果人们开始尝试比较AuthenticationMethods,这可能会引起一些有趣的问题。你可能还需要重载各种等式运算符。 - Ant
38
@Ant:我不需要这样做。由于我们只有每种身份验证方法的一个实例,从 Object 继承的引用相等性就可以很好地工作。 - Jakub Šturc
10
编译器负责这个。构造函数是私有的,所以你不能创建新的实例。此外,静态成员不能通过实例访问。 - Jakub Šturc
21
非常有趣。我必须亲自试验并理解它的用法,才意识到它的好处。这是一个公共的非静态类,但不能被实例化,只能访问其静态成员。基本上,它的行为像枚举。但最棒的部分是……静态成员的类型是该类的类型,而不是通用的字符串或整数。它是一种……[等等]……类型安全的枚举!感谢您帮助我理解。 - tyriker
5
我喜欢的是你还可以在枚举类型上定义额外的方法,比如给予分组行为(例如一周中某天的枚举类型,有一个名为IsWeekend的方法,如果所引用的实例是星期六或星期日则返回true)。 - anaximander
显示剩余21条评论

236

使用方法

Enum.GetName(Type MyEnumType,  object enumvariable)  

假设(假设Shipper是一个已定义的枚举)

Shipper x = Shipper.FederalExpress;
string s = Enum.GetName(typeof(Shipper), x);

还有许多其他值得探索的Enum类静态方法...


6
没错。我确实为字符串描述创建了一个自定义属性,但这是因为我想要一个用户友好的版本(带有空格和其他特殊字符),可以轻松地绑定到ComboBox或类似的控件上。 - lc.
5
Enum.GetName反映了枚举中的字段名称 - 与.ToString()相同。如果性能是一个问题,这可能是一个问题。除非你正在转换大量的枚举,否则不必担心它。 - Keith
8
如果你需要一个带有额外功能的枚举类型,可以考虑另一种选择,那就是使用结构体自己来创建。你可以添加静态只读命名属性来表示枚举值,并将其初始化为生成结构体实例的构造函数。请记住,不要改变原文意思。 - Charles Bretana
2
问题在于GetName不可本地化。这并非总是一个问题,但需要注意。 - Joel Coehoorn
1
只有在编译时显式地拥有枚举的特定成员才能起作用。如果你只有一个变量,在运行时可能会分配任何枚举成员,那么你就做不到这一点。 - Charles Bretana
显示剩余7条评论

81

您可以使用ToString()方法引用名称而不是值。

Console.WriteLine("Auth method: {0}", AuthenticationMethod.Forms.ToString());

文档在这里:

http://msdn.microsoft.com/en-us/library/16c1xs4z.aspx

......如果你使用帕斯卡命名法来为枚举命名(我通常这样做,例如ThisIsMyEnumValue = 1等),那么你可以使用一个非常简单的正则表达式来打印友好的格式:

static string ToFriendlyCase(this string EnumString)
{
    return Regex.Replace(EnumString, "(?!^)([A-Z])", " $1");
}

这个函数可以轻松地从任何字符串中调用:

Console.WriteLine("ConvertMyCrazyPascalCaseSentenceToFriendlyCase".ToFriendlyCase());

输出:

将我的疯狂的帕斯卡命名法句子转换为友好的大小写

这样就不用在枚举中创建自定义属性并将它们附加到枚举上,也不用使用查找表将枚举值与友好字符串匹配,最重要的是,它是自我管理的,并且可以用于任何帕斯卡大小写字符串,这使得它更具可重用性。当然,它不允许您拥有与枚举不同的友好名称,而您的解决方案提供了这一点。

不过,我确实喜欢你最初的解决方案,因为它适用于更复杂的场景。您可以进一步将您的GetStringValue方法作为您的枚举的扩展方法,那么您就不需要像StringEnum.GetStringValue这样引用它了...

public static string GetStringValue(this AuthenticationMethod value)
{
  string output = null;
  Type type = value.GetType();
  FieldInfo fi = type.GetField(value.ToString());
  StringValue[] attrs = fi.GetCustomAttributes(typeof(StringValue), false) as StringValue[];
  if (attrs.Length > 0)
    output = attrs[0].Value;
  return output;
}

你可以直接从枚举实例中轻松访问它:

Console.WriteLine(AuthenticationMethod.SSO.GetStringValue());

2
这并不能解决“友好名称”需要空格的情况,比如“表单身份验证”。 - Ray Booysen
4
请确认枚举名称使用大写字母,例如FormsAuthentication,并在任何不在开头的大写字母前插入空格。在字符串中插入空格并不难。 - BenAlabaster
4
如果Pascal Case名称中包含应大写的缩写词,例如XML或GPS,自动插入空格就会变得有问题。 - Richard Ev
3
@RichardEv,没有完美的正则表达式适用于此,但这里有一个可以更好地处理缩写词的正则表达式: "(?!^)([^A-Z])([A-Z])", "$1 $2"。因此,HereIsATEST 变成了 Here Is ATEST - sparebytes
不优雅地进行这些小“技巧”,这就是它们的本质。我理解OP的意思,正在尝试找到类似的解决方案,即利用枚举的优雅之处,但能够轻松访问相关的消息。我能想到的唯一解决方案是在枚举名称和字符串值之间应用某种映射,但这并不能解决维护字符串数据的问题(但使其适用于需要具有多个区域等场景)。 - Trevor

75

不幸的是,反射获取枚举属性的速度相当慢:

请参见此问题:有人知道快速获取枚举值上自定义属性的方法吗?

.ToString() 在枚举中也相当慢。

您可以为枚举编写扩展方法:

public static string GetName( this MyEnum input ) {
    switch ( input ) {
        case MyEnum.WINDOWSAUTHENTICATION:
            return "Windows";
        //and so on
    }
}

这不是最好的方法,但会快速完成而不需要反射属性或字段名。


C#6 更新

如果您可以使用 C#6,则新的 nameof 运算符适用于枚举,因此 nameof(MyEnum.WINDOWSAUTHENTICATION) 将在编译时转换为 "WINDOWSAUTHENTICATION",使其成为获取枚举名称的最快方法。

请注意,这将把显式的 enum 转换为内联常量,因此对于您在变量中拥有的枚举不起作用。所以:

nameof(AuthenticationMethod.FORMS) == "FORMS"

但是...

var myMethod = AuthenticationMethod.FORMS;
nameof(myMethod) == "myMethod"

24
您可以一次获取属性值并将它们放入一个Dictionary<MyEnum,string>中,以保持声明式方面。 - Jon Skeet
1
是的,当我们发现反射是瓶颈时,在一个有大量枚举的应用程序中,我们最终采取了这种做法。 - Keith
2
@user919426:想要实现什么?把它们放在一个字典里吗?只需创建一个字典,最好使用集合初始化程序...不清楚你在问什么。 - Jon Skeet
感谢您的回复@JonSkeet。抱歉,我的意思是要问实际的实现方法。如何“只获取一次”而不需要重新调用。我会使用您的答案来确定逻辑。 - tinonetic
nameof() 只有在您预先知道枚举实例的情况下才能正常工作。例如,在 var enumVal = MyEnum.Value; 中,nameof(enumVal) 返回的值将是 'enumVal' 而不是 'Value'。这是编译器的特性,而非运行时的特性。 - JoeBrockhaus
显示剩余5条评论

61

我使用一个扩展方法:

public static class AttributesHelperExtension
    {
        public static string ToDescription(this Enum value)
        {
            var da = (DescriptionAttribute[])(value.GetType().GetField(value.ToString())).GetCustomAttributes(typeof(DescriptionAttribute), false);
            return da.Length > 0 ? da[0].Description : value.ToString();
        }
}

现在用以下方式为enum进行装饰:

public enum AuthenticationMethod
{
    [Description("FORMS")]
    FORMS = 1,
    [Description("WINDOWSAUTHENTICATION")]
    WINDOWSAUTHENTICATION = 2,
    [Description("SINGLESIGNON ")]
    SINGLESIGNON = 3
}

当你调用 AuthenticationMethod.FORMS.ToDescription() 时,你将会得到 "FORMS"


1
我不得不添加 using System.ComponentModel;。此外,如果您想要字符串值与枚举名称相同,则此方法才有效。OP想要一个不同的值。 - elcool
3
你的意思是在调用AuthenticationMethod.FORMS.ToDescription()时,对吧? - nicodemus13
这个解决方案非常好,可以让描述与枚举变量名称不同。 - Michael Murphy

42

只需使用ToString()方法即可。

public enum any{Tomato=0,Melon,Watermelon}

要引用字符串Tomato,只需使用:

any.Tomato.ToString();

哇,这真的很简单。我知道原帖想要添加自定义字符串描述,但这就是我需要的。回想起来,我应该知道尝试这个方法,但我走了Enum.GetName的路线。 - Rafe
18
由于大多数情况下,您使用.ToString()方法得到的值与您需要的用户友好值不同。 - Novitchi S
2
@Brent - 这与所提出的问题不同。 所提出的问题是如何从已分配枚举值的变量中获取此字符串。 这是在运行时动态的。 这是在运行时检查类型定义和设置。 - Hogan
1
@Hogan - ToString() 方法也适用于变量: any fruit = any.Tomato; string tomato = fruit.ToString(); - LiborV
@LiborV - 请记住这是在09年编写的 - C#当时是不同的,对枚举实例调用ToString()方法会产生不同的结果。 - Hogan

31

使用 .Net 4.0 或更高版本可以轻松解决此问题,无需其他代码。

public enum MyStatus
{
    Active = 1,
    Archived = 2
}

要获取有关的字符串,只需使用:

使用以下代码来获取字符串:

MyStatus.Active.ToString("f");
或者
MyStatus.Archived.ToString("f");`

该值将为“Active”或“Archived”。

要查看调用Enum.ToString时的不同字符串格式(上述中的“f”),请参见此Enumeration Format Strings页面。


28

我使用System.ComponentModel命名空间中的Description属性。只需装饰enum,然后使用以下代码检索它:

public static string GetDescription<T>(this object enumerationValue)
            where T : struct
        {
            Type type = enumerationValue.GetType();
            if (!type.IsEnum)
            {
                throw new ArgumentException("EnumerationValue must be of Enum type", "enumerationValue");
            }

            //Tries to find a DescriptionAttribute for a potential friendly name
            //for the enum
            MemberInfo[] memberInfo = type.GetMember(enumerationValue.ToString());
            if (memberInfo != null && memberInfo.Length > 0)
            {
                object[] attrs = memberInfo[0].GetCustomAttributes(typeof(DescriptionAttribute), false);

                if (attrs != null && attrs.Length > 0)
                {
                    //Pull out the description value
                    return ((DescriptionAttribute)attrs[0]).Description;
                }
            }
            //If we have no description attribute, just return the ToString of the enum
            return enumerationValue.ToString();

        }

举个例子:

public enum Cycle : int
{        
   [Description("Daily Cycle")]
   Daily = 1,
   Weekly,
   Monthly
}

这段代码很好地处理了枚举类型,如果你不需要“友好名称”,则只会返回枚举的.ToString()。


28

我非常喜欢Jakub Šturc的回答,但它的缺点是你不能将它与switch-case语句一起使用。这里是他的答案的稍微修改版本,可以与switch语句一起使用:

public sealed class AuthenticationMethod
{
    #region This code never needs to change.
    private readonly string _name;
    public readonly Values Value;

    private AuthenticationMethod(Values value, String name){
        this._name = name;
        this.Value = value;
    }

    public override String ToString(){
        return _name;
    }
    #endregion

    public enum Values
    {
        Forms = 1,
        Windows = 2,
        SSN = 3
    }

    public static readonly AuthenticationMethod FORMS = new AuthenticationMethod (Values.Forms, "FORMS");
    public static readonly AuthenticationMethod WINDOWSAUTHENTICATION = new AuthenticationMethod (Values.Windows, "WINDOWS");
    public static readonly AuthenticationMethod SINGLESIGNON = new AuthenticationMethod (Values.SSN, "SSN");
}

因此,您将获得Jakub Šturc回答的所有好处,另外我们可以像这样在switch语句中使用它:

var authenticationMethodVariable = AuthenticationMethod.FORMS;  // Set the "enum" value we want to use.
var methodName = authenticationMethodVariable.ToString();       // Get the user-friendly "name" of the "enum" value.

// Perform logic based on which "enum" value was chosen.
switch (authenticationMethodVariable.Value)
{
    case authenticationMethodVariable.Values.Forms: // Do something
        break;
    case authenticationMethodVariable.Values.Windows: // Do something
        break;
    case authenticationMethodVariable.Values.SSN: // Do something
        break;      
}

一个更短的解决方案是删除枚举{},而是保留一个静态计数器来记录您已经构建了多少个枚举。这也有一个好处,即您不必将新实例添加到枚举列表中。例如,public static int nextAvailable { get; private set; },然后在构造函数中this.Value = nextAvailable++; - Slate
有趣的想法 @kjhf。但我的担忧是,如果有人重新排列代码,则分配给枚举值的值也可能会更改。例如,当将枚举值保存到文件/数据库时,更改“new AuthenticationMethod(...)”行的顺序(例如,删除其中一个),然后再次运行应用程序并从文件/数据库检索枚举值时,可能无法匹配最初保存的AuthenticationMethod的枚举值。 - deadlydog
好的观点 - 虽然我希望在这些特定情况下,人们不会依赖于枚举的整数值(或重新排序枚举代码)。--而这个值纯粹是用作开关和可能是.Equals()和.GetHashCode()的替代方法。如果担心,您可以随时放置一个带有“不要重新排序”的巨大注释:p - Slate
你不能只是重载 = 运算符来允许 switch 工作吗?我在 VB 中做到了这一点,现在可以在 select case 语句中使用它。 - user1318499
@user1318499 不,C#对于switch语句的规则比VB更加严格。你不能在Case语句中使用类实例,只能使用常量原始数据类型。 - deadlydog

13
我结合了上面几个建议,再加上一些缓存。我从网络上某处找到了一个代码,但现在既想不起来我是从哪里得到的,也找不到它了。如果有人发现类似的东西,请在评论中注明出处。
无论如何,使用涉及类型转换器,因此如果绑定到UI,它会“正常工作”。您可以通过从类型转换器初始化为静态方法来扩展Jakub的模式以进行快速代码查找。
基本用法如下:
[TypeConverter(typeof(CustomEnumTypeConverter<MyEnum>))]
public enum MyEnum
{
    // The custom type converter will use the description attribute
    [Description("A custom description")]
    ValueWithCustomDescription,

   // This will be exposed exactly.
   Exact
}

自定义枚举类型转换器的代码如下:
public class CustomEnumTypeConverter<T> : EnumConverter
    where T : struct
{
    private static readonly Dictionary<T,string> s_toString = 
      new Dictionary<T, string>();

    private static readonly Dictionary<string, T> s_toValue = 
      new Dictionary<string, T>();

    private static bool s_isInitialized;

    static CustomEnumTypeConverter()
    {
        System.Diagnostics.Debug.Assert(typeof(T).IsEnum,
          "The custom enum class must be used with an enum type.");
    }

    public CustomEnumTypeConverter() : base(typeof(T))
    {
        if (!s_isInitialized)
        {
            Initialize();
            s_isInitialized = true;
        }
    }

    protected void Initialize()
    {
        foreach (T item in Enum.GetValues(typeof(T)))
        {
            string description = GetDescription(item);
            s_toString[item] = description;
            s_toValue[description] = item;
        }
    }

    private static string GetDescription(T optionValue)
    {
        var optionDescription = optionValue.ToString();
        var optionInfo = typeof(T).GetField(optionDescription);
        if (Attribute.IsDefined(optionInfo, typeof(DescriptionAttribute)))
        {
            var attribute = 
              (DescriptionAttribute)Attribute.
                 GetCustomAttribute(optionInfo, typeof(DescriptionAttribute));
            return attribute.Description;
        }
        return optionDescription;
    }

    public override object ConvertTo(ITypeDescriptorContext context, 
       System.Globalization.CultureInfo culture, 
       object value, Type destinationType)
    {
        var optionValue = (T)value;

        if (destinationType == typeof(string) && 
            s_toString.ContainsKey(optionValue))
        {
            return s_toString[optionValue];
        }

        return base.ConvertTo(context, culture, value, destinationType);
    }

    public override object ConvertFrom(ITypeDescriptorContext context, 
       System.Globalization.CultureInfo culture, object value)
    {
        var stringValue = value as string;

        if (!string.IsNullOrEmpty(stringValue) && s_toValue.ContainsKey(stringValue))
        {
            return s_toValue[stringValue];
        }

        return base.ConvertFrom(context, culture, value);
    }
}

}


如何使用它?谢谢。例如:MyEnum.ValueWithCustomDescription.??() 或者其他什么? - Trương Quốc Khánh
这篇答案是十年前写的,所以我真的不确定原来的上下文。SO 的问题在于它永远存在。我相信这是关于添加数据以显示在 UI(如 WinForms 或 WPF)上的,此时只需将属性或集合绑定到 UI 控件即可直接获取类型转换器。 - Steve Mitcham

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接