Hello World
Spiga

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

2009-11-11 00:07 by 老赵, 17500 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之后,锁会占用很大一部分开销。

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

相关文章

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

26 条回复

  1. 醉春风
    *.*.*.*
    链接

    醉春风 2009-11-11 00:16:00

    真有精力。

  2. 老赵
    admin
    链接

    老赵 2009-11-11 00:30:00

    总之,这些个关于Custom Attribute的尝试,都是失败的,呵呵。

  3. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-11 00:44:00

    我是WebForm的坚定支持者 对MVC完全无爱

  4. 老赵
    admin
    链接

    老赵 2009-11-11 00:55:00

    @winter-cn
    这儿其实和MVC没有任何关系的……

  5. 韦恩卑鄙 alias:v-zhewg
    *.*.*.*
    链接

    韦恩卑鄙 alias:v-zhewg 2009-11-11 01:27:00

    (只可惜,即便是不考虑线程安全特性,从字典中查找对象的性能,可能还不如直接构造一个对象——更别说如果配合了ReaderWriterLockSlim之后,锁会占用很大一部分开销。)


    这个不赞同 我个人经验里面 一般的创建对象 比ReaderWriterLockSlim+Hash字典查找 合起来都要浪费性能
    前提,得是一个容易hash的key 貌似type不怎么样

  6. 韦恩卑鄙 alias:v-zhewg
    *.*.*.*
    链接

    韦恩卑鄙 alias:v-zhewg 2009-11-11 01:29:00

    话说 老赵这个时候怎么就不用t4了呢
    难道这不是这孙子最闪亮的时候么

  7. heros
    *.*.*.*
    链接

    heros 2009-11-11 08:51:00

    都整到快四更天了。真是好精神。

  8. JimLiu
    *.*.*.*
    链接

    JimLiu 2009-11-11 09:30:00

    @韦恩卑鄙 alias:v-zhewg
    这还真不见得……用老赵的FastReflectLib做反射还是相当快的。字典如果东西多了就慢了,而且读写锁也是挺影响速度的。

  9. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-11 09:34:00

    C#还有很多可以或者说应当实现的特性没有实现。

    比如说前阵子我很恼C#为什么匿名类型的属性不允许附加特性(Attribute),这是多么实用的功能。。。。

  10. JimLiu
    *.*.*.*
    链接

    JimLiu 2009-11-11 09:38:00

    @Ivony...
    也许是因为Attribute必须在编译时完成吧。
    (其实我觉得如果MS想,这个不构成问题)

  11. ppchen(陈荣林)
    *.*.*.*
    链接

    ppchen(陈荣林) 2009-11-11 09:40:00

    还是老赵研究的深啊
    我也遇到过这样的需求,不能这样做感觉美中不足

  12. 老赵
    admin
    链接

    老赵 2009-11-11 09:41:00

    韦恩卑鄙 alias:v-zhewg:
    (只可惜,即便是不考虑线程安全特性,从字典中查找对象的性能,可能还不如直接构造一个对象——更别说如果配合了ReaderWriterLockSlim之后,锁会占用很大一部分开销。)

    这个不赞同 我个人经验里面 一般的创建对象 比ReaderWriterLockSlim+Hash字典查找 合起来都要浪费性能
    前提,得是一个容易hash的key 貌似type不怎么样


    不会啊,我刚测试过的,一会儿我再测试一下找你来看。

  13. 老赵
    admin
    链接

    老赵 2009-11-11 09:42:00

    韦恩卑鄙 alias:v-zhewg:
    话说 老赵这个时候怎么就不用t4了呢
    难道这不是这孙子最闪亮的时候么


    workaround总归是有的,而且其实手写也不复杂,俺只是想说这个特性一少很不爽。
    例如……没法静态检查了,我完全可以传一个typeof(int)去ModelBinderAttribute里,对不?

  14. 老赵
    admin
    链接

    老赵 2009-11-11 09:43:00

    JimLiu:
    @韦恩卑鄙 alias:v-zhewg
    这还真不见得……用老赵的FastReflectLib做反射还是相当快的。字典如果东西多了就慢了,而且读写锁也是挺影响速度的。


    字典东西多,倒也不会怎么慢,毕竟是O(1)的访问速度——除非多到无比多(理论上比如超过int范围的n倍),那内存也装不下了,呵呵。
    是啊是啊,我接下来就要改进这方面问题,避免使用读写锁……

  15. 老赵
    admin
    链接

    老赵 2009-11-11 09:44:00

    Ivony...:
    C#还有很多可以或者说应当实现的特性没有实现。

    比如说前阵子我很恼C#为什么匿名类型的属性不允许附加特性(Attribute),这是多么实用的功能。。。。


    要说服编译器开发团队这些个功能的重要性……是很不容易的……

  16. 韦恩卑鄙 alias:v-zhewg
    *.*.*.*
    链接

    韦恩卑鄙 alias:v-zhewg 2009-11-11 10:19:00

    Ivony...:
    C#还有很多可以或者说应当实现的特性没有实现。

    比如说前阵子我很恼C#为什么匿名类型的属性不允许附加特性(Attribute),这是多么实用的功能。。。。


    同恼C#为什么匿名类型不允许实现接口

  17. feilng
    *.*.*.*
    链接

    feilng 2009-11-11 10:37:00

    微软的基因决定了,他追求实用,甚至是平庸
    在他的所有产品上你可以看到这一点,这里没有美,这不是他们的哲学

  18. 韦恩卑鄙 alias:v-zhewg
    *.*.*.*
    链接

    韦恩卑鄙 alias:v-zhewg 2009-11-11 11:22:00

    Jeffrey Zhao:

    韦恩卑鄙 alias:v-zhewg:
    (只可惜,即便是不考虑线程安全特性,从字典中查找对象的性能,可能还不如直接构造一个对象——更别说如果配合了ReaderWriterLockSlim之后,锁会占用很大一部分开销。)

    这个不赞同 我个人经验里面 一般的创建对象 比ReaderWriterLockSlim+Hash字典查找 合起来都要浪费性能
    前提,得是一个容易hash的key 貌似type不怎么样


    不会啊,我刚测试过的,一会儿我再测试一下找你来看。



    我也做了个测试 发现这个性能是和 类本身大小/创建方法的复杂程度相关的

            static void Main(string[] args)
            {
                var dic = new SyncDictionary<int, SomeClass>(2000000);
                for (var i = 1; i < 2000000; i++)
                {
                    var sc = new SomeClass(i);
                    dic.Add(i, sc);
                
                }
                var d1 = DateTime.Now.Ticks;
    
                //Create
                for (var i = 1; i < 2000000; i++)
                {
                    var sc = new SomeClass(i);
                  //  sc.Value++;
                }
                var d2 = DateTime.Now.Ticks;
                //Find
                for (var i = 1; i < 2000000; i++)
                {
                    var sc = dic[i];
                 //   sc.Value++;
                }
                var d3 = DateTime.Now.Ticks;
    
    
                Console.Write("{0}/{1}", d2 - d1, d3 - d2);
                Console.ReadKey();
            }
    
    


    性能测试简单了点 但是有点说明问题
    当 SomeClass 继承Attribute的时候
            class SomeClass:Attribute 
            {
    
                public    int Value { get; set; }
                public SomeClass(int value)
                {
                    Value = value;
                }
            
            }
    

    2184004/2964005 老赵说的正确

    
            class SomeClass:System.Data.Dataset
            {
    
                public    int Value { get; set; }
                public SomeClass(int value)
                {
                    Value = value;
                }
            
            }
    

    9672017/2652005 我的经验也没错

    顺便说 SyncDictionary 是用 ReaderWriterLockSlim包的Dictionary

  19. 老赵
    admin
    链接

    老赵 2009-11-11 11:26:00

    @韦恩卑鄙 alias:v-zhewg
    本来不就应该是这样的么……

  20. 老赵
    admin
    链接

    老赵 2009-11-11 11:27:00

    feilng:
    微软的基因决定了,他追求实用,甚至是平庸
    在他的所有产品上你可以看到这一点,这里没有美,这不是他们的哲学


    那么你可以看看F#设计的如何,这都是某个语言的选择和目标而已。
    如果要上升高度的话,Java追求的是简陋,繁琐和固步自封?
    看到微软就说XX话,这也太没有技术含量了啊,呵呵。

    就像上次看到新闻说,Win7销售好,会让微软迷失,加速微软灭亡。
    想唱衰的话,真是可以找出太多理由了……

  21. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-11 13:49:00

    Jeffrey Zhao:
    @winter-cn
    这儿其实和MVC没有任何关系的……


    果然没关系 呵呵
    我很少用Attribute 老赵你的钻研精神真令人佩服 居然想到泛型Attribute 要是我来研究 发现语言不支持就算了 肯定不会花这么多时间的。

  22. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-11-11 16:07:00

    用C++/CLI尝试声明泛型Attribute失败。。以前C++/CLI很少检查这些东西的……

  23. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-11 16:26:00

    装配脑袋:用C++/CLI尝试声明泛型Attribute失败。。以前C++/CLI很少检查这些东西的……




    VB.NET呢?

  24. 韦恩卑鄙 alias:v-zhewg
    *.*.*.*
    链接

    韦恩卑鄙 alias:v-zhewg 2009-11-11 17:39:00

    Jeffrey Zhao:
    @韦恩卑鄙 alias:v-zhewg
    本来不就应该是这样的么……


    等我再用 createinstance来一次
    阿~~~153488779/2980170


    其实是我错误的估计 attribute 这个类太轻量级了

  25. 老赵
    admin
    链接

    老赵 2009-11-11 17:53:00

    @韦恩卑鄙 alias:v-zhewg
    那个,其实现在有Expression.Compile了,搞Emit容易多了,呵呵。
    话说我已经写了一篇新文章了,瞅瞅去。

  26. Cheese
    *.*.*.*
    链接

    Cheese 2009-11-12 15:39:00

    正巧请教老赵一个问题。
    我想在业务逻辑层用AOP的方式做权限控制,需要把当前登录的用户信息作为Attribute的成员传进去(构造函数或者属性赋值),但是发现用对象的实例,是不行的,编译不过。
    就像这样:
    [Permission(loginContext)] //loginContext是个对象实例
    public DetailEntity GetDetail(int id)

    我的猜测也是: Attribute修饰在编译期进行.

    我也开始怀疑到底应不应该在业务逻辑层控制权限了,发现获取cookie,重定向到登录页,都不是很方便。

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我