也談用反射實現Enum→String映射:一種重視效能的方法 )

來源:互聯網
上載者:User

一、問題的提出

最近,部落格園有許多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,並且幾乎沒有考慮異常,歡迎大家提意見和建議。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.