原文:C# 获取与解析枚举类型的 DescriptionAttribute

System.ComponentModel.DescriptionAttribute 这个 Attribute,经常被用来为属性或事件提供说明,这个说明是可以被本地化的。在一些用户界面中,就可以利用这个 Attribute 提供一些额外的信息,就像 Visual Studio 中所做的,如图 1 所示:

图 1 可以看到,对 AutoSizeMode 的说明,被显示在了下面的框中。

但是,界面中的枚举项就没这么好的待遇了,C# 类库中并没有内建对枚举项的 DescriptionAttribute 的支持,就像上面的图所显示的那样,枚举项仍然是英文的。要想提供自己想要的说明,就需要自己来完成。

一、简单的实现

这个功能实现起来其实也很简单,就是通过反射去读取 DescriptionAttribute 的 Description 属性的值,代码如下所示:

/// <summary>
/// 返回枚举项的描述信息。
/// </summary>
/// <param name="value">要获取描述信息的枚举项。</param>
/// <returns>枚举想的描述信息。</returns>
public static string GetDescription(Enum value)
{
Type enumType = value.GetType();
// 获取枚举常数名称。
string name = Enum.GetName(enumType, value);
if (name != null)
{
// 获取枚举字段。
FieldInfo fieldInfo = enumType.GetField(name);
if (fieldInfo != null)
{
// 获取描述的属性。
DescriptionAttribute attr = Attribute.GetCustomAttribute(fieldInfo,
typeof(DescriptionAttribute), false) as DescriptionAttribute;
if (attr != null)
{
return attr.Description;
}
}
}
return null;
}

这段代码还是很容易看懂的,这里取得枚举常数的名称使用的是 Enum.GetName() 而不是 ToString(),因为前者更快,而且对于不是枚举常数的值会返回 null,不用进行额外的反射。

当然,这段代码仅是一个简单的示例,接下来会进行更详细的分析。

二、完整的实现

在给出更加完整的实现之前,先要说说这个 DescriptionAttribute 的问题。

我个人认为,对于枚举来说,这个说明更像是一个可以本地化的、更为友好的别名,而不是一个解释或说明。就拿开头图片里的 AutoSizeMode 这个枚举为例子,我们更希望看到的是“自动扩大或缩小”和“只能扩大”,而不是 MSDN 中的说明那样“控件根据它的内容增大或缩小。 不能手动调整该控件的大小。”和“控件可以根据其内容任意增大,但不会缩小至小于它的 Size 属性值。 窗体可以调整大小,但不能缩小到它所包含的任意控件被隐藏。”

所以,这里更适合的使用 DisplayNameAttribute,而不是 DescriptionAttribute。但可惜的是,DisplayNameAttribute 只能用于类、方法、属性或事件,字段被它无情的抛弃了,因此目前只能拿并不是很合适的 DescriptionAttribute 来凑和了。

吐槽完毕,开始说正事。首先来说,上面的那个函数还是很粗糙的,有很多情况都没有考虑,例如:如果给出的 value 并没有对应一个枚举常数,应该怎么办?

首先参考下 Microsoft 是怎么做的,下面是 Enum.ToString() 的做法:

  • 如果是应用 Flags 标志的枚举,且存在与此实例的值相等的一个或多个已命名常数的组合,会返回用分隔符分隔的常数名称列表。若
  • 实例的值不能等于已命名常数的组合,就返回原始值。
  • 如果未应用 Flags 标志,就返回原始值。

所以我也将采用类似的做法,但是对于实例的值不能等于已命名常数的组合的情况(上面的第二点),会返回能够匹配的常数名称+未被匹配的数字值,而不仅仅只是数字值,这样我看来会更方便一些。

拿 BindingFlags 枚举来举例子的话,对于值 129,如果直接使用 Enum.ToString(),会直接返回 129,但我认为返回 IgnoreCase, 128 是一个更好的选择。

下面先上代码:

/// <summary>
/// 返回指定枚举值的描述(通过
/// <see cref="System.ComponentModel.DescriptionAttribute"/> 指定)。
/// 如果没有指定描述,则返回枚举常数的名称,没有找到枚举常数则返回枚举值。
/// </summary>
/// <param name="value">要获取描述的枚举值。</param>
/// <returns>指定枚举值的描述。</returns>
public static string GetDescription(this Enum value)
{
Type enumType = value.GetType();
// 寻找枚举值的组合。
EnumCache cache = GetEnumCache(enumType.TypeHandle);
ulong valueUL = ToUInt64(value);
int idx = Array.BinarySearch(cache.Values, valueUL);
if (idx >= 0)
{
// 枚举值已定义,直接返回相应的描述。
return cache.Descriptions[idx];
}
// 不是可组合的枚举,直接返回枚举值得字符串形式。
if (!cache.HasFlagsAttribute)
{
return GetStringValue(enumType, valueUL);
}
List<string> list = new List<string>();
// 从后向前寻找匹配的二进制。
for (int i = cache.Values.Length - 1; i >= 0 && valueUL != 0UL; i--)
{
ulong enumValue = cache.Values[i];
if (enumValue == 0UL)
{
continue;
}
if ((valueUL & enumValue) == enumValue)
{
valueUL -= enumValue;
list.Add(cache.Descriptions[i]);
}
}
list.Reverse();
// 添加最后剩余的未定义值。
if (list.Count == 0 || valueUL != 0UL)
{
list.Add(GetStringValue(enumType, valueUL));
}
return string.Join(", ", list);
}

代码中的 GetEnumCache 会返回特定枚举类型的值和对应说明的缓存,这样能够避免每次都进行反射,可以显著提高性能。

枚举值的所有比较都是使用 UInt64 来完成的,这样更容易写代码(比直接拿着 object 去写更方便),而且在进行二分查找时效率也更高。

对于应用了 Flags 标志的枚举,二进制的匹配时从后向前的(注意 Values 是从小到大排序的),在最后再进行反转,这样就可以得到与 Enum.ToString() 相同的顺序。

而 GetStringValue 方法,就是获取枚举值对应的数字。但这里不能直接 ToString(),因为枚举值可以是负数,为了保证输出的值与定义的相同,需要根据枚举的基础类型进行判断,是否转换为 Int64 再输出。

三、枚举的解析

现在已经可以根据枚举得到相应的说明了,接下来要完成其逆过程——解析。解析过程大体说来就是下面的四步:

  1. 尝试将字符串作为数字解析,如果成功就不必进行代价更高的字符串匹配了。这里需要能够解析带正负号的整数,而且最大需要可以解析 UInt64 范围的整数,所以这里根据字符串的第一个字符是否是"-",来决定是使用 Int64.TryParse 方法还是 UInt64.TryParse 方法。
  2. 将字符串以“,”分隔为字符串数组。在这里,通常的做法是使用 string.Split(',') 来分割字符串,但这样做效率很低,而且还需要做一次 Trim() 以去除空白,因此会产生额外的字符串复制。所以我直接采用 IndexOf() + SubString() 来实现,更加高效,实现也并不算复杂。
  3. 解析数组中的每个字符串,尝试与枚举常数或说明进行匹配。这里就是将上一步取得的字符串与枚举的缓存进行一一比较。为了支持枚举常数和说明,需要进行两遍字符串比较,第一遍与枚举常数进行比较,第二遍与说明进行比较。这里没有使用字典,主要是由于字典需要创建两个(区分和不区分大小写),感觉不太值得,而且一般枚举常数都在 10 个以内,顺序查找也不算慢。
  4. 匹配失败的情况下,尝试将每个数组识别为数字。这里就是为了保证由 GetDescription 方法得到的字符串能够被正确的解析。

解析方法的代码如下所示:

public static object ParseEx(Type enumType, string value, bool ignoreCase)
{
ExceptionHelper.CheckArgumentNull(enumType, "enumType");
ExceptionHelper.CheckArgumentNull(value, "value");
if (!enumType.IsEnum)
{
throw ExceptionHelper.MustBeEnum(enumType);
}
value = value.Trim();
if (value.Length == 0)
{
throw ExceptionHelper.MustContainEnumInfo();
}
// 尝试对数字进行解析,这样可避免之后的字符串比较。
char firstChar = value[0];
ulong tmpValue;
if (ParseString(value, out tmpValue))
{
return Enum.ToObject(enumType, tmpValue);
}
// 尝试对描述信息进行解析。
EnumCache cache = GetEnumCache(enumType.TypeHandle);
StringComparison comparison = ignoreCase ?
StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
ulong valueUL = 0;
int start = 0;
do
{
// 去除前导空白。
while (char.IsWhiteSpace(value, start)) { start++; }
int idx = value.IndexOf(',', start);
if (idx < 0) { idx = value.Length; }
int nIdx = idx - 1;
// 去除后面的空白。
while (char.IsWhiteSpace(value, nIdx)) { nIdx--; }
if (nIdx >= start)
{
string str = value.Substring(start, nIdx - start + 1);
int j = 0;
// 比较常数值的名称和描述信息,先比较名称,后比较描述信息。
for (; j < cache.Names.Length; j++)
{
if (string.Equals(str, cache.Names[j], comparison))
{
// 与常数值匹配。
valueUL |= cache.Values[j];
break;
}
}
if (j == cache.Names.Length && cache.HasDescription)
{
// 比较描述信息。
for (j = 0; j < cache.Descriptions.Length; j++)
{
if (string.Equals(str, cache.Descriptions[j], comparison))
{
// 与描述信息匹配。
valueUL |= cache.Values[j];
break;
}
}
}
// 未识别的枚举值。
if (j == cache.Descriptions.Length)
{
// 尝试识别为数字。
if (ParseString(str, out tmpValue))
{
valueUL |= tmpValue;
}
else
{
// 不能识别为数字。
throw ExceptionHelper.EnumValueNotFound(enumType, str);
}
}
}
start = idx + 1;
} while (start < value.Length);
return Enum.ToObject(enumType, valueUL);
}

四、在 PropertyGrid 中显示枚举说明

要在界面中显示对象的属性,经常用到的控件就是 PropertyGrid 了。如果希望枚举的说明可以在 PropertyGrid 中显示,可以利用 TypeConverterAttribute 来做到这一点。

首先需要定义一个支持读取枚举说明的 EnumDescConverter 类,它可以直接继承自 TypeConverter 类,也可以继承自 EnumConverter。它需要做的就是将枚举值转换为字符串(ConvertTo)时,使用 GetDescription() 而不是 ToString()。在 ConvertFrom 时,也要支持枚举说明的解析。

using System;
using System.ComponentModel;
using System.Globalization; namespace Cyjb.ComponentModel
{
/// <summary>
/// 提供将 <see cref="System.Enum"/> 对象与其他各种表示形式相互转换的类型转换器。
/// 支持枚举值的描述信息。
/// </summary>
public class EnumDescConverter : EnumConverter
{
/// <summary>
/// 使用指定类型初始化 <see cref="EnumDescConverter"/> 类的新实例。
/// </summary>
/// <param name="type">表示与此转换器关联的枚举类型。</param>
public EnumDescConverter(Type type)
: base(type)
{ }
/// <summary>
/// 将指定的值对象转换为枚举对象。
/// </summary>
/// <param name="context"><see cref="System.ComponentModel.ITypeDescriptorContext"/>,
/// 提供格式上下文。</param>
/// <param name="culture">一个可选的 <see cref="System.Globalization.CultureInfo"/>。
/// 如果未提供区域性设置,则使用当前区域性。</param>
/// <param name="value">要转换的 <see cref="System.Object"/>。</param>
/// <returns>表示转换的 <paramref name="value"/> 的 <see cref="System.Object"/>。</returns>
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
string strValue = value as string;
if (strValue != null)
{
try
{
return EnumExt.ParseEx(this.EnumType, strValue, true);
}
catch (Exception ex)
{
throw ExceptionHelper.ConvertInvalidValue(value, this.EnumType, ex);
}
}
return base.ConvertFrom(context, culture, value);
}
/// <summary>
/// 将给定的值对象转换为指定的目标类型。
/// </summary>
/// <param name="context"><see cref="System.ComponentModel.ITypeDescriptorContext"/>,
/// 提供格式上下文。</param>
/// <param name="culture">一个可选的 <see cref="System.Globalization.CultureInfo"/>。
/// 如果未提供区域性设置,则使用当前区域性。</param>
/// <param name="value">要转换的 <see cref="System.Object"/>。</param>
/// <param name="destinationType">要将值转换成的 <see cref="System.Type"/>。</param>
/// <returns>表示转换的 <paramref name="value"/> 的 <see cref="System.Object"/>。</returns>
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture,
object value, Type destinationType)
{
ExceptionHelper.CheckArgumentNull(destinationType, "destinationType");
if (value != null && destinationType.TypeHandle.Equals(typeof(string).TypeHandle))
{
return EnumExt.GetDescription((Enum)value);
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
}

然后利用 [TypeConverter(EnumDescConverter)] 在需要的属性上标识出自己的转换器类,这样 PropertyGrid 上显示的就是想要的说明了。

public class TestClass
{
[TypeConverter(typeof(EnumDescConverter))]
public Tristate Value { get; set; } // 这里的 Tristate 就是一个应用了 DescriptionAttribute 的枚举。
}

图 2 界面中显示的枚举值已经被正确的显示为中文。

最后是相关代码的链接:

包含枚举的相关方法的类 EnumExt 的完整代码可见 https://github.com/CYJB/Cyjb/blob/master/Cyjb/EnumExt.cs

上面的 EnumDescConverter 可见 https://github.com/CYJB/Cyjb/blob/master/Cyjb/ComponentModel/EnumDescConverter.cs

C# 获取与解析枚举类型的 DescriptionAttribute的更多相关文章

  1. 获取枚举类型Description特性的描述信息

    C#中可以对枚举类型用Description特性描述. 如果需要对Description信息获取,那么可以定义一个扩展方法来实现.代码如下: public static class EnumExten ...

  2. .net工具类 获取枚举类型的描述

    一般情况我们会用枚举类型来存储一些状态信息,而这些信息有时候需要在前端展示,所以需要展示中文注释描述. 为了方便获取这些信息,就封装了一个枚举扩展类. /// <summary> /// ...

  3. C# 枚举类型的描述信息获取

    新建一个控制台方法,写好自己的枚举类型: 如图: 在里面添加获取描述的方法: 具体源码: 链接:http://pan.baidu.com/s/1nv4rGkp 密码:byz8

  4. Qt::WindowFlags枚举类型解析

    在使用Qt设计的时候经常会看到QWidget控件的构造函数出现下面这样一句话: QWidget(QWidget *parent=0,Qt::WindowFlags f=0) QWidget *pare ...

  5. 在WPF中使用变通方法实现枚举类型的XAML绑定

    问题缘起 WPF的分层结构为编程带来了极大便利,XAML绑定是其最主要的特征.在使用绑定的过程中,大家都普遍的发现枚举成员的绑定是个问题.一般来说,枚举绑定多出现于与ComboBox配合的情况,此时我 ...

  6. 从一个int值显示相应枚举类型的名称或者描述

    我正在做一个出入库管理的简单项目,在Models里定义了这样的枚举类型 public enum InOrOut { [Description("出库")] Out = , [Des ...

  7. .NET面试题解析(04)-类型、方法与继承

      系列文章目录地址: .NET面试题解析(00)-开篇来谈谈面试 & 系列文章索引 做技术是清苦的.一个人,一台机器,相对无言,代码纷飞,bug无情.须梦里挑灯,冥思苦想,肝血暗耗,板凳坐穿 ...

  8. Asp.Net 之 枚举类型的下拉列表绑定

    有这样一个学科枚举类型: /// 学科 /// </summary> public enum Subject { None = , [Description("语文") ...

  9. .NET枚举类型转为List类型

    如图所示这个竞卖状态,原先是在前端界面通过html代码写死的几个状态,现在需要改为动态加载.这个几个状态是定义的枚举类型. 1:定义一个枚举类型 /// <summary>    /// ...

随机推荐

  1. 举例说,Linux核心名单(两)

    使用列表 我认为最好的方式,成为熟悉的核心列表功能是看一些简单的例子,素材去更好的理解链表. 以下是一个样例.包括创建.加入.删除和遍历链表. <span style="font-si ...

  2. Cordova探险系列(一个)

    最早接触PhoneGap平台是在1年多之前,可以使用HTML.CSS和JavaScript跨平台来编写Android或者IOS设备程序.而且应用的核心代码不须要多少改动就行移植.确实让我感觉的到它应该 ...

  3. Android_Training

    http://wiki.eoeandroid.com/Android_Training Android小白成长之0基础篇

  4. Ubuntu14.04设备JDK

    1.设备JDK 打开命令直插式工具.输入以下三个命令: sudo add-apt-repository ppa:webupd8team/java sudo apt-get update sudo ap ...

  5. nyoj 322 Sort 【树阵】

    这个问题实际上是在测试树的数组. 代码: #include <cstdio> #include <cstring> int c[1005]; int lowbit(int x) ...

  6. 设计与实现简单而经常使用的权限系统(四):无需维护level,递归构建树

    第三篇中.我们通过维护节点的深度level,通过迭代全部的节点,仅仅须要一次,就构造了树.  本篇.换一种方式. 优点是:不维护节点的深度level,添加和改动节点时,也不用维护.递归实现,代码比較清 ...

  7. BZOJ 1901 Zju 2112 Dynamic Rankings 与更改的树董事长

    标题效果:给定一个序列,单点变化,询价区间k大. 思维:假设没有变化.然后划分树就可以解决,但树的分工仍然是一棵树,它不支持的变化. 主席舒变化实际上是在外带fenwick右护套层值段树,但正确的值线 ...

  8. 通过java.util.concurrent写多线程程序

    在JDK 1.5之前,要实现多线程的功能,得用到Thread这个类,通过这个类设计多线程程序,需要考虑性能,死锁,资源等很多因素,一句话,就是相当麻烦,而且很容易出问题.所幸的是,在JDK1.5之后, ...

  9. SOA两个接口通常用于实现更:SOAP vs REST

    SOA协作架构异构系统,因此,一个跨操作系统的需求.跨语言的通用信息交换格公式. SOAP和REST它们是基于消息正文文本,在跨平台方面相比二进制消息优点.因此,作为选择SOA实施通常用于界面.但SO ...

  10. Facebook 网页应用图文设置教程

    最近在弄一个项目,需要使用Facebook进行登陆并且获取用户Facebook相关的数据.网上查找有关Facebook应用设置教程,中文资料中,要么介绍的是N版之前的API,要么是App端的教程.Fa ...