一、問題的提出
最近,部落格園有許多blogger提出了為枚舉顯示中文名稱的文章,例如[讓枚舉成員顯示出中文資訊],[利用自訂屬性,定義枚舉值的詳細文本],[細節決定成敗:映射枚舉],[利用DescriptionAttribute定義枚舉值的描述資訊],還有原來看過的一些文章(不好意思地址沒記)。這些文章的共同特點就是,使用了自訂Attribute附加在枚舉值上, 在運行時擷取枚舉相關的資訊。
這種方法中,由於是使用反射,因為有些人關心其中的效能問題——特別是處理大量資料的時候,例如將大量枚舉匯入到DataGrid的時候;而且人們也發現,Enum本身的ToString方法也使用了反射的方法,因此實際上也存在著速度慢的問題。本文試著以效能為重點,在不失去結構的易讀性、可擴充性的條件下,基於以上各位高手的經驗,給出一種重視效能的方法。
設計目標:
1,枚舉定義形式上使用容易讀寫的附加Attribute的形式;
2,支援多語言版本,可以很容易地被本地化;
3,調用格式簡單。
二、ToString()的效能問題
對於一個枚舉值emItem,用下列代碼進行測試:
for (int i = 0; i < 1000000; i++)
{
s = emItem.ToString();
}
其中s是String類型,emItem是一個MyEnum類型的枚舉。
在我的機器上,該迴圈要花費4900毫秒左右。
當我把其中的“s = emItem.ToString();”換成“s = Enum.GetName(typeof(MyEnum), emItem);”之後,這個時間減少到2300毫秒。
但是必須注意的是,ToString方法和GetName的方法並不是相同的;但是有些時候對於我們來說也許用哪個都可以。
因此我的第一個建議就是,如果可以互換的話,使用GetName代替ToString。
三、 反射的效能問題
顯然,上面的兩個方法ToString和GetName,都不能解決顯示枚舉的自訂名,以及提供不同語言版本的問題。因此,很多人採用了反射的方法,像下面這樣為每個枚舉值增加了Attribute:
public enum MyEnum
{
[EnumItemDescription("Description1")]
EnumValue1 = 1,
[EnumItemDescription("Description2")]
EnumValue2 = 2,
[EnumItemDescription("Description3")]
EnumValue3 = 4,
}
其中,EnumItemDescriptionAttribute是類似於DescriptionAttribute的類。
這樣做起來的確非常優雅;在讀取該Attribute的值時,大多數使用的是如下的格式:
static string GetStringFromEnum(Enum enumvalue)
{
FieldInfo finfo = enumvalue.GetType().GetField(enumvalue.ToString());
object[] cAttr = finfo.GetCustomAttributes(typeof(EnumItemDescriptionAttribute), true);
if (cAttr.Length > 0)
{
EnumItemDescriptionAttribute desc = cAttr[0] as EnumItemDescriptionAttribute;
if (desc != null)
{
return desc.Description;
}
}
return enumvalue.ToString();
}
事實上,這已經是簡化的模式——它沒有進行迴圈。實際上看到的許多blogger的程式中,對所有的FieldInfo進行迴圈,逐一比較其名字,然後還要對每個FieldInfo的每個Attribute進行迴圈——也就是說,複雜度是O(n^2)。
那麼,當我們用“s = GetStringFromEnum(emItem);”來進行我們進行的第一個實驗時,結果是多少呢?
結果是,當我等到30秒的時候我終於不耐煩了;當我正想強行關閉它的時候,它結束了——32秒,即3萬2千毫秒。
想想看,它慢也是當然的——每次將一個枚舉值對應為字串時,都要進行反射調用,而且每次還都要調用Enum.ToString這個本來就慢騰騰的傢伙!
四、Dictionay + Reflection的緩衝式實現嘗試
我們回頭來想一想,我們為什麼必須,或者說更喜歡在這裡使用反射?
因為如果不用反射,我們就必須寫一個像下面這樣的映射函數:
static string StringFromEnum(MyEnum enumValue)
{
switch (enumValue)
{
case MyEnum.EnumValue1:
return "String1";
case MyEnum.EnumValue2:
return "String2";
case MyEnum.EnumValue3:
return "String3";
}
return enumValue.ToString();
}
(或者我們也可以用一個Dictionary<MyEnum, string>來維護)
也就是說,這樣就把“枚舉值”和“枚舉值的名字”割裂開來了;從設計的角度來說,這樣的確為以後的維護增加了困難;但是這樣做的速度的確很快。
那麼,我們如果把這二者結合起來,不就完美了嗎?首先用反射讀取所有的Attribute,然後將之儲存到一個列表備用;以後每次調用時,不再進行反射調用,而是查詢這個列表(相當於緩衝)不就可以了嗎?程式如下:
public class EnumMap
{
private Type internalEnumType;
private Dictionary<Enum, string> map;
public EnumMap(Type enumType)
{
if (!enumType.IsSubclassOf(typeof(Enum)))
{
throw new InvalidCastException();
}
internalEnumType = enumType;
FieldInfo[] staticFiles = enumType.GetFields(BindingFlags.Public | BindingFlags.Static);
map = new Dictionary<Enum, string>(staticFiles.Length);
for (int i = 0; i < staticFiles.Length; i++)
{
if (staticFiles[i].FieldType == enumType)
{
string description = "";
object[] attrs = staticFiles[i].GetCustomAttributes(typeof(EnumItemDescriptionAttribute), true);
description = attrs.Length > 0 ?
((EnumItemDescriptionAttribute)attrs[0]).Description :
//若沒找到EnumItemDescription標記,則使用該枚舉值的名字
description = staticFiles[i].Name;
map.Add((Enum)staticFiles[i].GetValue(enumType), description);
}
}
}
public string this[Enum item]
{
get
{
if (item.GetType() != internalEnumType)
{
throw new ArgumentException();
}
return map[item];
}
}
}
這樣,我們只需要首先建立一個該類型的執行個體:
EnumMap myEnumMap = new EnumMap(typeof(MyEnum));
然後,在任何需要映射枚舉值為字串的地方,像這樣調用:
s = myEnumMap[emItem];
就可以了。
那麼,使用“s = myEnumMap[emItem];”進行最開始的哪個測試,結果如何呢?
結果是650毫秒——是不用“緩衝”時耗費時間的50分之一。
這裡我們注意到,直接提供EnumMap類可能會造成若干問題,而且對於每種枚舉類型,我們都要為之建立一個EnumMap對象,比較麻煩;
因此我們對其進行如下簡單封裝,一方面保證其Singleton特性,一方面不用再去一個個建立EnumMap對象了。
查看該類完整代碼
public class EnumMapHelper
{
// maps用於儲存每種枚舉及其對應的EnumMap對象
private static Dictionary<Type, EnumMap> maps;
// 由於C#中沒有static indexer的概念,所以在這裡我們用靜態方法
public static string GetStringFromEnum(Enum item)
{
if (maps == null)
{
maps = new Dictionary<Type, EnumMap>();
}
Type enumType = item.GetType();
EnumMap mapper = null;
if (maps.ContainsKey(enumType))
{
mapper = maps[enumType];
}
else
{
mapper = new EnumMap(enumType);
maps.Add(enumType, mapper);
}
return mapper[item];
}
private class EnumMap
{
private Type internalEnumType;
private Dictionary<Enum, string> map;
public EnumMap(Type enumType)
{
if (!enumType.IsSubclassOf(typeof(Enum)))
{
throw new InvalidCastException();
}
internalEnumType = enumType;
FieldInfo[] staticFiles = enumType.GetFields(BindingFlags.Public | BindingFlags.Static);
map = new Dictionary<Enum, string>(staticFiles.Length);
for (int i = 0; i < staticFiles.Length; i++)
{
if (staticFiles[i].FieldType == enumType)
{
string description = "";
object[] attrs = staticFiles[i].GetCustomAttributes(typeof(EnumItemDescriptionAttribute), true);
description = attrs.Length > 0 ?
((EnumItemDescriptionAttribute)attrs[0]).Description :
//若沒找到EnumItemDescription標記,則使用該枚舉值的名字
description = staticFiles[i].Name;
map.Add((Enum)staticFiles[i].GetValue(enumType), description);
}
}
}
public string this[Enum item]
{
get
{
if (item.GetType() != internalEnumType)
{
throw new ArgumentException();
}
return map[item];
}
}
}
}
調用時,形式簡單,只需要一條語句即可:
s = EnumMapHelper.GetStringFromEnum(emItem);
五、對多語言的支援
對多語言的支援方面,我們只需要照著FCL的樣子畫瓢就可以了:
public class EnumItemDescriptionAttribute : DescriptionAttribute
{
private bool replaced;
public EnumItemDescriptionAttribute(string description)
: base(description)
{
}
public override string Description
{
get
{
if (!this.replaced)
{
this.replaced = true;
base.DescriptionValue = SR.GetString(base.Description);
}
return base.Description;
}
}
}
其中的SR是同步讀取資源的類,與內容關係不大,這裡就略去了(可以參考FCL的SR的實現)。
到此為止,一個快速(比Enum本身的ToString方法還要快4倍),形式簡潔(無論是聲明形式還是調用形式),支援多語言的映射類就完成了。
其中可能有若干bug,並且幾乎沒有考慮異常,歡迎大家提意見和建議。