原本打算两篇写在一起,但是我认为这两个话题本身并没有太大关联,因此分开,便于查询。其实在构建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会影响编译效果,因此它必须在编译期“完成”。


“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之后,锁会占用很大一部分开销。

因此,还是很遗憾的。不过事在人为,在受限的环境下研究提升性能的方式也有别样的乐趣。