一次失败的尝试(下):无法使用泛型的Attribute
2009-11-11 00:07 by 老赵, 17559 visits原本打算两篇写在一起,但是我认为这两个话题本身并没有太大关联,因此分开,便于查询。其实在构建Attribute的时候,我们经常会从构造函数中传入一个Type类型,然后在Attribute中使用Activator.CreateInstance或其他的“反射”方法来构造对象。那么,我忽然想,为什么不能使用泛型的Attribute呢?
例如,ASP.NET MVC的ModelBinderAttribute是这样定义的:
[AttributeUsage(ValidTargets, AllowMultiple = false, Inherited = false)] public sealed class ModelBinderAttribute : CustomModelBinderAttribute { public ModelBinderAttribute(Type binderType) { // 省略各种校验 ... BinderType = binderType; } public Type BinderType { get; private set; } public override IModelBinder GetBinder() { try { return (IModelBinder)Activator.CreateInstance(BinderType); } catch (Exception ex) { ... } } }
但是,我很想设计出这样的ModelBinderAttribute:
public class ModelBinderAttribute<TBinder> : CustomModelBinderAttribute where TBinder : IModelBinder, new() { public override IModelBinder GetBinder() { return new TBinder(); } }
于是,我们便可以这样使用Attribute标记:
[ModelBinder<SomeBinder>]
从这个例子中便可以看出泛型类的优点,由于我们可以添加泛型约束,因此就直接保证了TBinder的类型是IModelBinder而无须校验,也可以轻易地使用默认构造函数来创建对象,从而避免了反射的开销。但问题就来了,这段代码没法编译通过,其错误便是:
A generic type cannot derive from 'CustomModelBinderAttribute' because it is an attribute class.
这不禁令人大失所望,但这又是为什么呢?似乎在这里有一些说法。有人说:
Attribute修饰在编译期进行,但是泛型类只有在运行时才能获得最终的类型信息。由于Attribute会影响编译效果,因此它必须在编译期“完成”。这篇MSDN文章有更多信息。
“Attribute影响编译效果”这点有些道理(例如AttributeUsageAttribute),但是我认为编译期其实只会识别一些特别的标记,而更多的标记只是在运行时才使用的。
此外,也有人指出ECMA-334中的说法:
ECMA-334,14.16节写到:“下列某些环境下必须使用常量表达式,如果编译期无法完整求出表达式的值,则会出现编译错误。”Attribute在列表中
但我认为,其实只要在标记时提供了明确的类型(即[ModelBinder<SomeBinder>],而不是[ModelBinder<T> where...]),编译器还是可以在编译期间得到完整信息的。而ECMA这段文字只是对Attribute的“参数”提出了要求,个人认为并没有提及Attribute类型。
而我比较认可的是被标记为答案的说法:
那个,我不知道为什么没法这么做,但我可以确定这不是CLI的问题。CLI规约(spec)并没有提到这点(至少我没发现),而且其实你可以使用IL来直接创建泛型的Attribute。只不过,C# 3的规约禁止了这一点——10.1.4节“基类规约”并没有给出任何理由。
注释版的ECMA C# 2规约也没有提供任何有用的信息,尽管它给出了一些不允许的示例。C# 3规约的注释版应该明天就到了……我想看看它里面有没有更多说明。不管怎样,这肯定是个语言的约束,而不是一个运行时的限制。
修改:Eric Lippert的答复(总结)是,没有什么特别的原因,只是为了避免增加语言和编译器的复杂度,这个功能看上去并没有太大帮助。
那么这点在C# 4里有没有改进呢?答案是否定的,Eric Lippert已经明确了这一点:
没错(指C# 4不会有这个功能)。这个功能在优先级列表中的重要性还是很低。
无论这个特性是否真的重要,但是这的确给我带来了一定不便。这并不仅仅是缺少了静态检查等等,更重要的是少了显式的泛型参数,有些做法就无法实现了。例如,如果我要根据不同的TBinder来缓存它的实例,那么我本可以使用“泛型字典”的方式:
public static class ModelBinderCache<TBinder> where TBinder: IModelBinder, new() { static ModelBinderCache() { Instance = new TBinder(); } public static TBinder Instance { get; private set; } }
使用这种方式来“保存数据”,使用T来获取Instance的性能非常高,而且它的静态构造函数还是线程安全的,这为我们省了很多事情。如果像现在那样,我们只能获得一个Type对象,那么唯一可做的只能是使用字典进行存储了。只可惜,即便是不考虑线程安全特性,从字典中查找对象的性能,可能还不如直接构造一个对象——更别说如果配合了ReaderWriterLockSlim之后,锁会占用很大一部分开销。
因此,还是很遗憾的。不过事在人为,在受限的环境下研究提升性能的方式也有别样的乐趣。
真有精力。