原文: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. overflow的几个坑

    在android 4.0的原生浏览器上注意: html元素上不要加overflow: auto;的样式,否则会造成有些元素无法点击 在absolute元素上 不要加 overflow: auto; 否 ...

  2. [课程分享]IT软件项目管理(企业项目甘特如是评价、维护管理、文档管理、风险管理、人力资源管理)

    [课程分享]IT件项目管理(企业项目甘特图案例评价.维护管理.文档管理.风险管理.人力资源管理) 对这个课程有兴趣的朋友能够加我的QQ2059055336和我联系 课程讲师:丁冬博士 课程分类:Jav ...

  3. 前端project师的修真秘籍(css、javascript和其他)

    以我的经验,大部分技术,熟读下列四类书籍就可以. 入门,用浅显的语言和方式讲述正确的道理和方法,如head first系列 全面,巨细无遗地探讨每一个细节,遇到疑难问题时往往能够在这里得到理论解答,如 ...

  4. 基于Gsoap 的ONVIF C++ 库

    https://github.com/xsmart/onvifcpplib 该库支持ProfileS 和ProfileG,目前正在开发哪些,现拥有支持Event 下面是一个client样本 int _ ...

  5. 模拟Vue之数据驱动3

    一.前言 在"模拟Vue之数据驱动2"中,我们实现了个Observer构造函数,通过它可以达到监听已有数据data中的所有属性. 但,倘若我们想在某个对象中,新增某个属性呢? 如下 ...

  6. ORACLE 创建表空间、用户、授权

    1.创建表空间 create tablespace TEST  logging datafile 'e:\app\administrator\oradata\orcl\TEST.dbf' size 1 ...

  7. hdu4570Multi-bit Trie (间隙DP)

    Problem Description IP lookup is one of the key functions of routers for packets forwarding and clas ...

  8. 余弦信号DFT频谱分析(继续)

    以前谈到序列的实际长度可以通过零填充方法加入,使得最终增加N添加表观分辨率. 但它并没有解决泄漏频率的问题. 根本原因在于泄漏窗口选择的频率. 由于矩形窗突然被切断,频谱旁瓣相对幅度过大,造成泄漏分量 ...

  9. hdu2571命

    称号: Problem Description 穿过幽谷意味着离大魔王lemon已经无限接近了! 可谁能想到,yifenfei在斩杀了一些虾兵蟹将后.却再次面临命运大迷宫的考验.这是魔王lemon设下 ...

  10. (大数据工程师学习路径)第三步 Git Community Book----中级技能(下)

    一.追踪分支 1.追踪分支 在Git中‘追踪分支’是用于联系本地分支和远程分支的. 如果你在’追踪分支'(Tracking Branches)上执行推送(push)或拉取(pull)时,它会自动推送( ...