代码改变世界

一次失败的尝试(下):无法使用泛型的Attribute

2009-11-11 00:07 Jeffrey Zhao 阅读(...) 评论(...) 编辑 收藏

原本打算两篇写在一起,但是我认为这两个话题本身并没有太大关联,因此分开,便于查询。其实在构建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之后,锁会占用很大一部分开销。

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

相关文章