Hello World
Spiga

如果是能简单解决的问题,就不用想得太复杂了

2009-09-05 17:41 by 老赵, 7026 visits

有个朋友在MSN问我说,有没有关于Emit的资料,它想生成一个类的动态代理。他抱怨Emit还是很麻烦,不过交谈过后知道他是想要做什么。他希望为一个对象的某个属性作延迟加载,这样可以避免一些无谓的消耗。例如:

public class SomeClass
{
    public int SomeID { get; set; }

    // some other members...
}

原本构造一个SomeClass时可以这样:

var someClass = new SomeClass();
someClass.SomeID = GetSomeID();
Process(someClass);

但是由于Process方法中可能不需要用到SomeID属性,于是在外部调用的GetSomeID方法可能就形成了无谓的性能损耗。一个常见的做法方式可能就是进行延迟加载了。那位朋友的意思是先把SomeID标为virtual:

public class SomeClass
{
    public virtual int SomeID { get; set; }

    // some other members...
}

然后使用Emit来生成一个动态类型,继承SomeClass,override掉SomeID属性,形成延迟加载。不过我提出,这个方法是不是太重了,因为动态代理不是那么孤立存在的,它往往需要考虑很多其他东西。例如缓存动态类型,例如,对于相同类型一个成员或多个成员的延迟加载,使生成一个通用的动态类型,还是多个动态类型。例如……怎么样的API是最合适的?

所以,如果只是简单的情况下,不如直接手动来实现这样的延迟效果:

public class LazySomeClass : SomeClass
{
    public override int SomeID
    {
        get
        {
            return this.LazySomeID.Value;
        }
        set
        {
            this.LazySomeID.Value = value;
        }
    }

    public Lazy<int> LazySomeID { get; set; }
}

于是在使用的时候就可以:

var someClass = new LazySomeClass();
someClass.LazySomeID = new Lazy<int>(() => GetSomeID());
Process(someClass);

这样其实就可以在一定程度上达到目的了。Lazy类的原理在之前也有过提及(这里需要些修改),这是一种简单但有用的类型。其实在项目的许多情况下,我们这么做也足够了。不需要复杂的方法,不需要复杂的Emit。不过如果您是为了锻炼能力,或者由于项目中此类需求特别多,想设计一个通用的的类库,这也不错。

当然,上面的实现也有缺陷,因为它不是最理想、最完整、最通用的延迟加载代理类(为什么?)。如果您感兴趣,也可以想象一个完美的代理类应该是什么样子的,甚至给出一个通用的辅助类库。

哦,对了,NHibernate的做法其实也不完美,有机会我会分析一下,并阐述我的看法的。

Creative Commons License

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

Add your comment

34 条回复

  1. 鹤冲天
    *.*.*.*
    链接

    鹤冲天 2009-09-05 17:47:00

    为了一个属性的延迟加载,动用Emit,太不划算了。

  2. 鹤冲天
    *.*.*.*
    链接

    鹤冲天 2009-09-05 18:03:00

    我刚发的随笔中提到了.Net中的一个延迟加载,System.Collections.Generic.EqualityComparer<T>.Default,实现如下:

    public static EqualityComparer<T> Default
    {
        get
        {
            EqualityComparer<T> defaultComparer = EqualityComparer<T>.defaultComparer;
            if (defaultComparer == null)
            {
                defaultComparer = EqualityComparer<T>.CreateComparer();
                EqualityComparer<T>.defaultComparer = defaultComparer;
            }
            return defaultComparer;
        }
    }
    

    这个属性中使用了一个局部变量,是线程安全的。

  3. 鹤冲天
    *.*.*.*
    链接

    鹤冲天 2009-09-05 18:07:00

    现在想想又不是了,没有使用 double check 啊!

  4. 老赵
    admin
    链接

    老赵 2009-09-05 18:10:00

    @鹤冲天
    你这不是线程安全的,最多只能说EqualityComparer<T>.CreateComparer()调用多遍不会有问题而已。
    你的写法和下面这种其实没有太大区别:

    public static EqualityComparer<T> Default
    {
        get
        {
            if (EqualityComparer<T>.defaultComparer == null)
            {
                EqualityComparer<T>.defaultComparer = EqualityComparer<T>.CreateComparer();
            }
    
            return EqualityComparer<T>.defaultComparer;
        }
    }
    
    一段代码是不是线程安全,不是用没用局部变量的问题。
    我之前的文章是在谈double check,不是在谈局部变量。

  5. 老赵
    admin
    链接

    老赵 2009-09-05 18:11:00

    @鹤冲天
    呵呵,你也意识到了啊。不过有时候也没有关系。比如你的场景这么写也不会出错。

  6. 鹤冲天
    *.*.*.*
    链接

    鹤冲天 2009-09-05 18:19:00

    @Jeffrey Zhao
    看了你前几天发那篇随笔,心里老想着这种情况下要用个局部变量,一定要用,反倒把 double check“忘”了。
    刚才发的代码是从.Net中反编译的,这篇随笔中你又说到延迟加载,正好对上了。一想啊,局部变量用上了,很安全的,.Net写的真好。兴奋了好一会,才发现不对,最重要的double check漏了...

  7. 老赵
    admin
    链接

    老赵 2009-09-05 18:22:00

    @鹤冲天
    其实也不是非得需要局部变量,我那篇文章的意思只是在double check中“关键性操作应该是原子性的”,用局部变量只是一个手段。
    例如,给一个字段赋值是原子性的,因此不需要一个临时变量。

  8. 鹤冲天
    *.*.*.*
    链接

    鹤冲天 2009-09-05 18:30:00

    @Jeffrey Zhao
    还是你理解深入、透彻。

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

    Ivony... 2009-09-05 18:41:00

    我觉得老赵最近的风格在变,在向好的方向发展。无论是架构师还是开发人员,我们的目的不是写很炫,很酷,很强大通用可以被膜拜的代码,而是解决一个又一个实际的问题。

  10. 老赵
    admin
    链接

    老赵 2009-09-05 18:43:00

    @Ivony...
    这……难道我以前的风格是写“很炫,很酷,很强大通用可以被膜拜的代码”吗?
    其实我基本没变,只是我以前觉得像现在一些经常写的问题不值得记录下来而已,呵呵。

  11. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-09-05 19:12:00

    呵呵。可能是那个太重了一下子引起了我的共鸣吧,说了几句跑题的话。忽而有感,写个博客。。。。

  12. 杨同学
    *.*.*.*
    链接

    杨同学 2009-09-05 19:38:00

    有同感,支持老赵!

  13. Jeff Wong
    *.*.*.*
    链接

    Jeff Wong 2009-09-05 19:44:00

    我是冲着标题来的。读懂老赵的意思了,顶。

  14. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-09-05 21:42:00

    嘿嘿……要说SRE(System.Reflection.Emit)难用的话,多半是指IL生成的部分?其它部分还是很直观的,基本上C#代码怎么写,Assembly/Type/Method-Builder就怎么用。ILGenerator的使用涉及MSIL的细节,确实不够直观。
    于是到广告时间:.NET 4.0/DLR里的ETv2(Expression Tree v2)里,System.Linq.LambdaExpression类上有一个CompileToMethod方法,可以直接通过新的ETv2来构造方法体,不必把抽象层次降到IL一层,非常直观,跟直接写C#感觉差不多(前提是你知道你的C#代码的语义到底是什么……)

  15. 老赵
    admin
    链接

    老赵 2009-09-05 21:58:00

    @RednaxelaFX
    嗯,Emit复杂的就是IL的复杂性,比如要处理装箱拆箱,类型转换,Nullable<T>到T等等。

  16. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-09-05 22:01:00

    @Jeffrey Zhao
    所以有了.NET 4.0/DLR之后,这个问题不再困难……至少没有以前那么困难。ETv2自身的编译器会帮忙解决问题的,连嵌套的LambdaExpression都可以处理,里面用到了非局部变量的话还会自动生成合适的Closure类型,非常、非常方便……

  17. 老赵
    admin
    链接

    老赵 2009-09-05 22:17:00

    @RednaxelaFX
    其实我正在用它写个……文章里Lazy的通用类库玩……

  18. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-09-06 02:26:00

    老赵以前的风格是偏向著书立说的 一般是泼水也不漏的 让人讨论也无处下嘴椰~

    估计现在也觉得大家讨论讨论确实效果不错
    开始把自己有腹稿的东西拿出来大家头脑风暴一下

    有集体智慧 比一个人搜肠刮肚开心
    也经常能有很好的观点迸出来

    远离寂寞 有益身心 :D

  19. 老赵
    admin
    链接

    老赵 2009-09-06 03:26:00

    @韦恩卑鄙
    卑鄙兄就是犀利啊,哈哈。

  20. oldrev
    *.*.*.*
    链接

    oldrev 2009-09-06 08:54:00

    这是一个设计问题吧,需要把外部函数保存到属性里以便随后调用,怎么看怎么别扭,还是调整设计,把这个 lazy set 的需求改成标准 lazy load 的:

    public class LazySomeClass : SomeClass
    {
    public BigBlob SomeID
    {
    get
    {
    return m_bigBlob ?? m_bigBlob = new BigBlob();
    }
    set
    {
    m_bigBlob = value;
    }
    }

    private BigBlob m_bigBlob;
    }

  21. oldrev
    *.*.*.*
    链接

    oldrev 2009-09-06 08:55:00

    差了个括号
    public class LazySomeClass : SomeClass
    {
    public BigBlob SomeID
    {
    get
    {
    return m_bigBlob ?? (m_bigBlob = new BigBlob());
    }
    set
    {
    m_bigBlob = value;
    }
    }

    private BigBlob m_bigBlob;
    }

  22. Stranger
    *.*.*.*
    链接

    Stranger 2009-09-06 09:43:00

    真是高产,佩服!

  23. Galactica
    *.*.*.*
    链接

    Galactica 2009-09-06 11:12:00

    Jeffrey Zhao:
    @鹤冲天
    你这不是线程安全的,最多只能说EqualityComparer<T>.CreateComparer()调用多遍不会有问题而已。
    你的写法和下面这种其实没有太大区别:

    public static EqualityComparer<T> Default
    {
        get
        {
            if (EqualityComparer<T>.defaultComparer == null)
            {
                EqualityComparer<T>.defaultComparer = EqualityComparer<T>.CreateComparer();
            }
    
            return EqualityComparer<T>.defaultComparer;
        }
    }
    
    一段代码是不是线程安全,不是用没用局部变量的问题。
    我之前的文章是在谈double check,不是在谈局部变量。



    的确是线程安全的,EqualityComparer<T>.CreateComparer()是静态方法,并且它其中的逻辑不包括任何其它静态字段,并发调用只会瞬时产生多个defaultComparer。

    而你修改后的写法,就不一定了,EqualityComparer<T>.defaultComparer 极有可能在EqualityComparer<T>.CreateComparer()执行途中就获得有效引用,这样另一个线程通过这个不完全构造的对象就会产生错误。

  24. Seasun海豚
    *.*.*.*
    链接

    Seasun海豚 2009-09-06 11:41:00

    只能偷偷地看了。哎~~~我还有几年可以挥霍啊。

  25. 老赵
    admin
    链接

    老赵 2009-09-06 14:07:00

    @oldrev
    为什么这是标准LazyLoad啊?
    其实你这个方法和我的基本没有区别,你只是直接new了,而我是提供了一个属性值的生成器。
    在实际情况中,你也不能直接new,然后就变成我的做法了,呵呵。

  26. 疯流成性
    *.*.*.*
    链接

    疯流成性 2009-09-06 22:42:00

    在实际情况中,你也不能直接new ???

    实际情况就是没看过文章之前就是这样做的。
    不过你的方法更好,更抽象。学习了

  27. 老赵
    admin
    链接

    老赵 2009-09-06 22:46:00

    @疯流成性
    这倒不是抽象的问题,我的意思是,再实际使用过程中,延迟加载的值是外部提供的啊,所以我说,类的内部不知道new出什么对象来的……

  28. oldrev
    *.*.*.*
    链接

    oldrev 2009-09-07 09:09:00

    @Jeffrey Zhao
    这就是我说的设计问题了,new 不 new 不是问题的关键,关键是谁来实现 lazy load。
    个人认为 Lazy load 应该转移成为类内部的责任,因为是这个类拥有需要延迟加载的属性,拥有者不是应该比使用者知道的多吗?

  29. 老赵
    admin
    链接

    老赵 2009-09-07 09:16:00

    @oldrev
    但是对于Data Transfer Object,数据是外部给予的,职责是外部的。
    例如如果是普通情况下:
    var user = new User();
    user.Name = "Jeffrey Zhao";
    user.Gender = "M";

    但是我现在需要延迟加载:
    var user = new User();
    user.LazyName = () => "Jeffrey Zhao"
    user.Gender = "M";

    这里的设计目标就是让外部来构造一个延迟加载的DTO,使用手动构造也好,使用类库也好,是不能变的。
    你说的“职责”是普通情况下一个表示行为的类的职责,我认为不适用于DTO。

  30. 老赵
    admin
    链接

    老赵 2009-09-07 09:19:00

    @oldrev
    DTO的问题讨论完了,再来讨论非DTO的情况。
    的确,延迟加载这个“行为”也可以属于Model的。
    当时,Model要加载什么,还是要有外部给。
    例如,你可以:
    User user = new User(() => "Jeffrey Zhao");
    无论如何,这个值总归是需要注入的,或者说委派给其他对象。
    你可能也会写:
    User user = new User(new NameLoader());
    但是无论如何,在实际情况下,几乎是不会直接使用new的。
    所以我说“你这个方法和我的基本没有区别”,“实际使用过程中,延迟加载的值是外部提供的”,“不能直接new”。

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

    装配脑袋 2009-09-07 22:49:00

    @鹤冲天
    这重点不在延迟加载,因为一个Comparer的默认实现是没有状态的,延迟不延迟都无所谓,也没有对线程安全的任何要求。这里利用的是一个泛型极其重要的特性——XX
    但愿你能早日发现它的强大,嘿嘿。不然就翻翻我以前的博客吧:P

  32. 老赵
    admin
    链接

    老赵 2009-09-07 22:52:00

    @装配脑袋
    话说我刚犯了一个错误,估计就和你现在说的东西有关。
    明天秀一下,让大家一起欣赏一下这个错误。

  33. 鹤冲天
    *.*.*.*
    链接

    鹤冲天 2009-09-08 08:26:00

    @装配脑袋
    莫非是:泛型类为每个型都保存一组静态字段或属性。

  34. oldrev
    *.*.*.*
    链接

    oldrev 2009-09-08 09:33:00

    DTO 这个东西本身就是比较搞笑的,定义一个几乎和 Model 一样的对象只是为了能序列化?而且相互转换的时候居然需要一个一个属性手工赋值,麻烦的要死。

    在实践中我始终倾向于使用 Dictionary 或者 DataRow 来做相同的事情,直接用反射就处理了。

发表回复

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

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

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

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

使用Live Messenger联系我