Santé

为明天干杯!
posts - 47, comments - 318, trackbacks - 9, articles - 0
  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理
一、问题的提出

最近,博客园有许多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[0as 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对象了。
查看该类完整代码

调用时,形式简单,只需要一条语句即可:
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,并且几乎没有考虑异常,欢迎大家提意见和建议。

Feedback

#1楼   回复  引用  查看    

2006-04-25 16:10 by henry      
不错,不过代码是不是应该加上线程安全锁呢.

#2楼   回复  引用  查看    

2006-04-25 16:15 by cncxz(虫虫)      
学习~~

#3楼[楼主]   回复  引用  查看    

2006-04-25 16:16 by smalldust      
@henry
是的。应该要加。这里作为示例代码省略掉了。

#4楼   回复  引用  查看    

2006-04-25 16:30 by 临波      
如果数据量大的话,是否做缓存就要看是不是值得的了,占很大内存

#5楼[楼主]   回复  引用  查看    

2006-04-25 16:50 by smalldust      
@临波
你误会了,缓存的是枚举的Description属性值,例如文中的MyEnum枚举,缓存的是三个字符串Description1,Description2,Description3。

#6楼   回复  引用  查看    

2006-04-25 21:31 by 双鱼座      
事实上我在任何使用反射的场合都是配合缓存一起使用的。与你的方式不同的是,我没有引入额外的类型,而是用Attribute类型本身,虽然获得的好处仅仅只是少构造一个实例。
关于线程锁,其实仅仅在建立这个对照表的时候才需要互斥锁,所以,用静态方法实现Singleton模型是非常好的选择。

#7楼   回复  引用  查看    

2006-04-26 07:01 by C# hack      
果真是搞研究的!

#8楼   回复  引用  查看    

2006-04-26 13:56 by 维生素C.NET      
受益匪浅

#9楼   回复  引用  查看    

2006-04-26 15:28 by 最笨的那个      
hehe,不错,比我那个强多了
http://sukyboor.cnblogs.com/archive/2006/04/22/382173.html">让枚举成员显示出中文信息
性能这边我只是考虑用一个hastable实现缓存

#10楼   回复  引用  查看    

2006-04-27 16:44 by 韦恩卑鄙      
这个有必要让m$集成到开发环境里去 哼

#11楼   回复  引用  查看    

2006-04-28 09:06 by 最笨的那个      
哪倒不必,这个问题是因为汉语在中国的地位越来越低造成的
弄的大伙都羞于用汉子,嘎嘎

#12楼   回复  引用    

2006-05-07 20:52 by JasonShen[未注册用户]
我照你的代码写了,怎么编译不通过:
1 “System.SR”不可访问,因为它受保护级别限制错误
2 “System.SR”并不包含“GetString”的定义

楼主能否给个完整代码?

#13楼   回复  引用    

2006-06-07 09:56 by ssj[未注册用户]
我是直接使用的SR来定义,比如:

MyEnum_EnumValue1 = 枚举值1
MyEnum_EnumValue2 = 枚举值2

即直接以枚举类型和枚举名称作为key,这样直接就可以用SR来获取描述字符串了

#14楼   回复  引用    

2007-02-27 00:32 by 晕死[未注册用户]
一个简单的枚举要搞这么复杂,如果需要更大的灵活性,枚举类型显然不是一个好方法,用单例工厂模式,我记得在Java中 java.util.logging.Level 中这样定义一个常数性质的实例,用起来和枚举差不多,只是在C#中不支持Switch结构

public static final Level WARNING = new Level("WARNING", 900, defaultBundle);

#15楼   回复  引用    

2007-06-22 16:08 by danjiewu[未注册用户]
测下这个呢

private static Dictionary<Enum, string> enumDescriptionCache = new Dictionary<Enum, string>();

public static string GetDescription(Enum item)
{
if (!enumDescriptionCache.ContainsKey(item))
{
FieldInfo enumField = item.GetType().GetField(item.ToString());
DescriptionAttribute[] attrs = (DescriptionAttribute[])enumField.GetCustomAttributes(typeof(DescriptionAttribute), true);
enumDescriptionCache[item] = attrs.Length > 0 ? attrs[0].Description : enumField.Name;
}
return enumDescriptionCache[item];
}

#16楼   回复  引用  查看    

2008-04-26 14:35 by 留恋星空      
mark



发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 384657




相关文章:

相关链接: