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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM