枚举的 ToString 方法如何返回易于理解的字符串?

366

我的枚举包含以下值:

private enum PublishStatusses{
    NotCompleted,
    Completed,
    Error
};

我希望能以用户友好的方式输出这些值。
但我不需要再将字符串转换回值。


1
可能是C#字符串枚举的重复问题。 - nawfal
27个回答

443

我使用来自 System.ComponentModel 命名空间的 Description 属性。只需装饰枚举:

private enum PublishStatusValue
{
    [Description("Not Completed")]
    NotCompleted,
    Completed,
    Error
};

然后使用以下代码来检索它:

public static string GetDescription<T>(this T 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();
}

17
这个例子更易于阅读。https://dev59.com/ZnM_5IYBdhLWcg3wWRuV - RayLoveless
45
我怀疑按照这种解决方案使用反射会导致显著的性能下降。威尔方法中使用ToFriendlyString扩展方法的代码更容易理解,而且它的性能应该也非常快。 - humbads
5
这并不意味着每个枚举都需要拥有自己的扩展方法。这是更通用的用法,需要更多的工作,但在我们决定性能之前,你可能想量化一下“快速”具体指什么。 - Ray Booysen
1
此外,您可以编写一些可能令人恐惧的代码来缓存这些值。由于它们是编译进去的,您可以在扩展方法之外的静态Dictionary<T,string>字典中静态缓存描述属性的值。这取决于您对代码气味的看法。 - Ray Booysen
2
@petar,这可以运行,但如果您想向用户显示友好的字符串,则无法使用。MY_TYPE将拥有下划线并且不可定制。 - Ray Booysen
显示剩余15条评论

433

我使用扩展方法来实现这个:

public enum ErrorLevel
{
  None,
  Low,
  High,
  SoylentGreen
}

public static class ErrorLevelExtensions
{
  public static string ToFriendlyString(this ErrorLevel me)
  {
    switch(me)
    {
      case ErrorLevel.None:
        return "Everything is OK";
      case ErrorLevel.Low:
        return "SNAFU, if you know what I mean.";
      case ErrorLevel.High:
        return "Reaching TARFU levels";
      case ErrorLevel.SoylentGreen:
        return "ITS PEOPLE!!!!";
      default:
        return "Get your damn dirty hands off me you FILTHY APE!";
    }
  }
}

11
这比属性回答整洁多了。太好了! - pennyrave
4
嗯,许多UI组件都希望找到并使用DisplayNameAttribute和DescriptionAttribute。事实上,现在我使用这些属性和扩展方法来轻松获取这些值。 - user1228
81
我认为这个问题在于你不断地编写这些扩展方法。使用属性机制,只需简单地装饰它并仅调用一个方法即可。 - Ray Booysen
6
不确定您的意思? - Ray Booysen
13
在我看来,更好的做法是允许default分支的实现返回me.ToString(),并且只为你想要覆盖的枚举值提供switch case语句。在你的例子中,我知道它们都是不同的,但在实际应用中,我认为大多数单词枚举值就足够了,你只需要为多个单词的枚举值提供覆盖。 - Scott
显示剩余17条评论

100
也许我漏掉了什么,但是 Enum.GetName 有什么问题吗?
public string GetName(PublishStatusses value)
{
    return Enum.GetName(typeof(PublishStatusses), value)
}

编辑:要创建用户友好的字符串,您需要通过 .resource 进行国际化/本地化处理,使用基于枚举键的固定键比在相同属性上使用装饰器属性更好。


13
我会返回枚举的字面值,而不是一些用户友好的值。 - Boris Callens
2
oic - 好的,那么基于这个值,你需要通过一个字符串资源库来处理一个相当大的案例,因为另一种选择(装饰器属性)不支持国际化(I18N)。 - annakata
1
在国际化(I18N)的情况下,我会让GetDescription()方法在资源库中搜索已翻译的字符串,然后回退到描述,最后回退到字面意思。 - Boris Callens
3
+1 表示将 MyEnum.ToString() 作为本地化资源键。我已经这样做了好几年。 - jackvsworld
1
@annakata,我们实际上扩展了属性机制以支持l18N,这实际上是一个简单的更改。 - Ray Booysen
显示剩余2条评论

29
我创建了一个反向扩展方法,将描述转换回枚举值:
public static T ToEnumValue<T>(this string enumerationDescription) where T : struct
{
    var type = typeof(T);

    if (!type.IsEnum)
        throw new ArgumentException("ToEnumValue<T>(): Must be of enum type", "T");

    foreach (object val in System.Enum.GetValues(type))
        if (val.GetDescription<T>() == enumerationDescription)
            return (T)val;

    throw new ArgumentException("ToEnumValue<T>(): Invalid description for enum " + type.Name, "enumerationDescription");
}

16
很抱歉,但感谢您的帮助!然而,因为这是一个问答网站,答案应该是直接回答问题的尝试。而且问题明确指出“我不需要再将字符串转换为值。”再次感谢! - Jesse
12
感谢您的积极批评。作为一个新手,了解网站的文化和细微差别总是很困难的。我很高兴有像您这样的人能够纠正新手的错误。再次感谢您没有对新手指手画脚。 - Brian Richardson
12
四年后,有人很高兴在这里找到了bjrichardson的代码!虽然SO是一个问答网站,但这并不意味着一旦问题得到回答就会被遗忘。 - John
1
@Jesse 十年后,有人很高兴在这里找到了Brian Richardson的代码! - Whitebrim
1
像Stack Overflow这样的地方的好处在于它们保留了像Jesse这样的互联网纳粹的迹象,他们更关心网站规范和跟上潮流,而不是内容是否真正有帮助。 - Chris

20

最简单的解决方法是使用自定义扩展方法(至少在.NET 3.5中 - 您可以将其转换为早期框架版本的静态帮助器方法)。

public static string ToCustomString(this PublishStatusses value)
{
    switch(value)
    {
        // Return string depending on value.
    }
    return null;
}

我在这里假设你想返回枚举值的实际名称之外的其他内容(你可以通过简单地调用ToString来获取它)。


虽然有效,但我更喜欢属性方式。这样,我可以将我的toString方法放在一个单独的库中,同时将自定义字符串表示与枚举本身放在一起。 - Boris Callens
1
可以的。我认为这种方法的一个优点是,你可以在调用方法时包含一个参数来指定某个状态变量,然后根据这个变量的值改变所返回的字符串表示。 - Noldorin
1
是的,我想这完全取决于方法的范围。虽然使用属性的方式更加通用,但你的解决方案更加局部化。最终关键在于需求。 - Boris Callens
1
你可以将扩展方法放在任何地方。只需在想要使用它们的地方引用即可。 - user1228
是的,但这意味着每次引入一个新的枚举类型时,都需要重新编写此扩展方法以获得友好名称。这也意味着您的所有应用程序都将携带其他所有应用程序的友好名称... - Boris Callens
CS1106 拓展方法必须在非泛型静态类中定义。 - shinzou

13

那篇帖子是关于Java的。在C#中,您无法将方法放入枚举中。

只需像这样做:

PublishStatusses status = ...
String s = status.ToString();

如果你想为枚举值使用不同的显示值,你可以使用属性和反射。


3
toString 在某些情况下并不安全——当枚举类型中存在多个相同值的条目(例如整数枚举)时,它会返回第一个匹配值的键,而不是被测试项的键。这就是为什么 Enum.GetName 更受推荐的原因。 - annakata
4
这是对于他特定枚举的最简单解决方案。 - Lemmy

12

还有一些更原始的选项,可以避免使用类/引用类型:

  • 数组方法
  • 嵌套结构体方法

数组方法

private struct PublishStatusses
{
    public static string[] Desc = {
        "Not Completed",
        "Completed",
        "Error"
    };

    public enum Id
    {
        NotCompleted = 0,
        Completed,
        Error
    };
}

用法

string desc = PublishStatusses.Desc[(int)PublishStatusses.Id.Completed];

嵌套结构体方法

private struct PublishStatusses
{
    public struct NotCompleted
    {
        public const int Id = 0;
        public const string Desc = "Not Completed";
    }

    public struct Completed
    {
        public const int Id = 1;
        public const string Desc = "Completed";
    }

    public struct Error
    {
        public const int Id = 2;
        public const string Desc = "Error";
    }            
}

用法

int id = PublishStatusses.NotCompleted.Id;
string desc = PublishStatusses.NotCompleted.Desc;

更新(2018年03月09日)

一种扩展方法和上面第一种技术的混合体。

我更喜欢将枚举定义在它们“属于”的地方(靠近它们来源的地方,而不是在某个通用的全局命名空间中)。

namespace ViewModels
{
    public class RecordVM
    {
        //public enum Enum { Minutes, Hours }
        public struct Enum
        {
            public enum Id { Minutes, Hours }
            public static string[] Name = { "Minute(s)", "Hour(s)" };
        }
    }
}

扩展方法似乎适用于常见领域,而枚举的“本地化”定义现在使扩展方法更加冗长。

namespace Common
{
    public static class EnumExtensions
    {
        public static string Name(this RecordVM.Enum.Id id)
        {
            return RecordVM.Enum.Name[(int)id];
        }
    }   
}

枚举及其扩展方法的使用示例。

namespace Views
{
    public class RecordView 
    {
        private RecordDataFieldList<string, string> _fieldUnit;

        public RecordView()
        {
            _fieldUnit.List = new IdValueList<string, string>
            {            
                new ListItem<string>((int)RecordVM.Enum.Id.Minutes, RecordVM.Enum.Id.Minutes.Name()),
                new ListItem<string>((int)RecordVM.Enum.Id.Hours, RecordVM.Enum.Id.Hours.Name())
            };
        }

        private void Update()
        {    
            RecordVM.Enum.Id eId = DetermineUnit();

            _fieldUnit.Input.Text = _fieldUnit.List.SetSelected((int)eId).Value;
        }
    }
}
注意:我实际上决定取消Enum包装器(和Name数组),因为最好的方式是将名称字符串来自于资源(即配置文件或数据库)而不是硬编码,并且因为我最终将扩展方法放在ViewModels命名空间中(只是在不同的“CommonVM.cs”文件中)。再加上整个.Id的事情变得令人分心和繁琐。
namespace ViewModels
{
    public class RecordVM
    {
        public enum Enum { Minutes, Hours }
        //public struct Enum
        //{
        //    public enum Id { Minutes, Hours }
        //    public static string[] Name = { "Minute(s)", "Hour(s)" };
        //}
    }
}

CommonVM.cs

//namespace Common
namespace ViewModels
{
    public static class EnumExtensions
    {
        public static string Name(this RecordVM.Enum id)
        {
            //return RecordVM.Enum.Name[(int)id];
            switch (id)
            {
                case RecordVM.Enum.Minutes: return "Minute(s)";                    
                case RecordVM.Enum.Hours: return "Hour(s)";
                default: return null;
            }
        }
    }   
}

枚举类型和其扩展方法的用法示例。

namespace Views
{
    public class RecordView 
    {
        private RecordDataFieldList<string, string> _fieldUnit

        public RecordView()
        {
            _fieldUnit.List = new IdValueList<string, string>
            {            
                new ListItem<string>((int)RecordVM.Enum.Id.Minutes, RecordVM.Enum.Id.Minutes.Name()),
                new ListItem<string>((int)RecordVM.Enum.Id.Hours, RecordVM.Enum.Id.Hours.Name())
            };
        }

        private void Update()
        {    
            RecordVM.Enum eId = DetermineUnit();

            _fieldUnit.Input.Text = _fieldUnit.List.SetSelected((int)eId).Value;
        }
    }
}

1
+1-1=0 票:这个解决方案保留了Enum语法,且优雅地解决了问题,而不需要反射或复杂的代码,所以在这方面是加一。但它损失了Enums本身的特性。因此,虽然我认为这是一个不错的选择,但它并没有回答实际的问题,所以得到了减一。总体来说,评分为0。很抱歉我们在SO中没有更好的记录方式。 - TonyG
@TonyG 好的。在错过 pluralsight.com 的 .net 技能评估中的一些问题后,我开始意识到 C# 枚举的深度,所以在决定应用哪种方法论之前,至少了解它们的能力可能是一个好主意(特别是对于普遍使用,重构可能需要花费一些时间;)。 - samus

8

最简单的方法就是将此扩展类包含在您的项目中,它可以与项目中的任何枚举一起使用:

public static class EnumExtensions
{
    public static string ToFriendlyString(this Enum code)
    {
        return Enum.GetName(code.GetType(), code);
    }
}

使用方法:

enum ExampleEnum
{
    Demo = 0,
    Test = 1, 
    Live = 2
}

...

ExampleEnum ee = ExampleEnum.Live;
Console.WriteLine(ee.ToFriendlyString());

2
这是一个谜,为什么这个评论不是被接受的或者最受欢迎的 - 没有反映,没有不必要的属性,非常适合简单的情况,其中枚举已经被很好地命名。你可以进一步完善这个答案,并允许在返回"My Enum"之前添加大写字母之间的空格。 - Vix
14
如果枚举值的名称本身已经足够清晰明了,就不需要使用扩展方法了。直接使用现有的ToString()方法即可。 string result = "Result: " + ee; - John
这应该是最好的答案。 它适用于任何枚举。 您甚至可以通过将参数的枚举类型更改为要使用它的实际枚举来使用特定的Enum实现它。 - Juanu
8
这个答案和所有的评论都忽略了原始请求中对更详细说明的要求。你们完全忽略了本次练习的目的,即返回除默认ToString值以外的内容。我不会为此在这里对所有笔记进行点踩,但我肯定想这么做。 - TonyG

7

您可以使用Humanizer软件包,具有枚举人类化的可能性。一个例子:

enum PublishStatusses
{
    [Description("Custom description")]
    NotCompleted,
    AlmostCompleted,
    Error
};

然后您可以直接在枚举上使用Humanize扩展方法:

var st1 = PublishStatusses.NotCompleted;
var str1 = st1.Humanize(); // will result in Custom description

var st2 = PublishStatusses.AlmostCompleted;
var str2 = st2.Humanize(); // will result in Almost completed (calculated automaticaly)

1
它也使用反射,而且不被缓存。https://github.com/Humanizr/Humanizer/blob/2e45bca3d4bfc8c9ff651a32490c8e7676558f14/src/Humanizer/EnumHumanizeExtensions.cs - Konrad
1
它将会和Ray的第一个答案中的解决方案一样慢。 - Konrad

6
public enum MyEnum
{
    [Description("Option One")]
    Option_One
}

public static string ToDescriptionString(this Enum This)
{
    Type type = This.GetType();

    string name = Enum.GetName(type, This);

    MemberInfo member = type.GetMembers()
        .Where(w => w.Name == name)
        .FirstOrDefault();

    DescriptionAttribute attribute = member != null
        ? member.GetCustomAttributes(true)
            .Where(w => w.GetType() == typeof(DescriptionAttribute))
            .FirstOrDefault() as DescriptionAttribute
        : null;

    return attribute != null ? attribute.Description : name;
}

3
总是很好写一些文字来解释为什么这应该有效,以及为什么OP的方法不行。 - phaberest
仅供参考,C# 代码规范要求局部变量和方法参数使用小写字母开头。唯一的例外是扩展方法中的 this 参数,在许多网络示例中可以看到其被称为 This。像您所做的那样将其类型作为调用方式 (Enum Enum) 会使代码不够易读。 - Massimiliano Kraus

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