Hello World
Spiga

不同泛型参数区分的独立类型

2012-12-03 23:11 by 老赵, 3861 visits

相对于的Java的“类型擦除(Type Erasure)”来说,.NET中的泛型可谓是真正的泛型,这让我们可以有能力区分运行时所使用的不同的具体类型,大大增强了程序设计的性能和表现能力。

打个比方,在Java 8中终于引入了Lambda表达式,但是由于它的伪泛型只能是一个“引用类型”而不能是“基础类型”,因此我们没法从int数组发起函数式操作,最后也没法回到List<char>这种类型(事实上这种类型在Java中根本不存在)。这除了影响编程体验和表达能力以外,对于内存和性能都有大量额外的开销。试想,谁希望在找出符合条件的一万个int数值的时候,必须额外创建一万个Integer对象,导致堆上增加几百上千K的空间,还有一万个对象带来的GC压力?当然,这次我们暂时不谈这方面,还是来谈谈.NET中“真泛型”这一特点所带来的编程便利。

在.NET中,我们编写一个泛型类型的时候,只会给出一个泛型类型的定义,例如List<T>,我们检查typeof(List<>).IsGenericDefinition也会得到true。然而,程序在真正运行的时候,使用的都是提供了具体泛型参数的类型,例如List<int>或是List<string>。我们没法创建一个“泛型定义”的实例或是访问它的静态成员等等,最多使用反射来访问它的信息。

在运行过程中,.NET运行时会(在第一次使用时)为不同的值类型创建一份不同的代码,而让所有的引用类型共享同一份代码。这是因为,假如T是值类型,那么生成的代码操作的都是栈上的数据,需要操作的字节数会有所不同,而引用类型都只需要操作16或32字节的地址,是一致的。当然,理论上List<long>List<DateTime>是可以共享代码的,因为它们其实都只是一个64字节的长整型,但是还是有些情况,尽管都是64字节的长度,如List<long>List<double>就不能共享代码。因此,运行时就统一为不同的值类型都创建不同的代码了。这的确会带来一定的额外开销,但在我看来,相比“真泛型”带来的便利,这点开销完全是值得的。想要了解更多这方面的内容,可以参考著名的Joe Duffy同学《On generics and (some of) the associated overheads》这篇文章。

不过无论执行的代码是否共享,不同具体类型参数的类型都是各自独立的,它们各有各的元数据,各有各的需方法表等等,因此它们的静态成员也是各自独立的。之前我也写过这方面的文章,例如它可能会让人上当,也可以利用这点写出高效的实现。这里我可以举出后者的另一个例子,例如在.NET中的ConcurrentDictionary实现中需要知道当前TValue类型的读写操作是否是原子的,它的实现就是这样的:

// Whether TValue is a type that can be written atomically (i.e., with no danger of torn reads)
private static readonly bool s_isValueWriteAtomic = IsValueWriteAtomic();

/// <summary>
/// Determines whether type TValue can be written atomically 
/// </summary> 
private static bool IsValueWriteAtomic()
{
    Type valueType = typeof(TValue);

    //
    // Section 12.6.6 of ECMA CLI explains which types can be read and written atomically without
    // the risk of tearing.
    // 
    // See http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-335.pdf 
    //
    bool isAtomic =
        (valueType.IsClass)
        || valueType == typeof(Boolean)
        || valueType == typeof(Char)
        || valueType == typeof(Byte)
        || valueType == typeof(SByte)
        || valueType == typeof(Int16)
        || valueType == typeof(UInt16)
        || valueType == typeof(Int32)
        || valueType == typeof(UInt32)
        || valueType == typeof(Single);

    if (!isAtomic && IntPtr.Size == 8)
    {
        isAtomic |= valueType == typeof(Double) || valueType == typeof(Int64);
    }

    return isAtomic;
}

我相信,假如让很多同学来实现这部分逻辑的话,就会创建IsValueWriteAtomic这样的静态方法,然后在需要的时候反复调用。但事实上,由于不同的泛型参数所带来的具体类型完全独立,因此我们完全可以像.NET那样将这个函数的结果保存在一个静态变量中,然后每次访问即可。

这个特性有时还可以帮助我们简化一些代码,举个最简单的例子:

public class SingletonBase<T> where T : new()
{
    public static readonly T Instance = new T();
}

public class MySingleton1 : SingletonBase<MySingleton1>
{
    // ...
}

public class MySingleton2 : SingletonBase<MySingleton2>
{
    // ...
}

这样我们就可以不用在每个类型中加上一个只读的Instance静态成员了。当然,这个例子简单地几乎没有实用意义,我们以后会来讨论更有价值的使用案例。

Creative Commons License

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

Add your comment

23 条回复

  1. 杨博
    183.14.172.*
    链接

    杨博 2012-12-04 00:32:16

    Java的确会搞类型擦除,不过Scala有个@specialized注解这种事我会随便告诉你吗?

  2. 老赵
    admin
    链接

    老赵 2012-12-04 09:49:58

    @杨博

    搞得好像我不知道Scala一样,所以我也推荐Scala么。Scala真是良心,太不容易了。

  3. 张然
    182.48.109.*
    链接

    张然 2012-12-04 11:06:39

    啥时候看老赵的文章能无压力啊,各种不理解赛~

  4. 六子
    220.231.59.*
    链接

    六子 2012-12-04 13:42:12

    老赵一直是高端java黑啊,就怕这种懂行的高级黑啊。:) 不友好的java泛型,偶也不喜欢。。。

  5. Quibbler
    125.70.0.*
    链接

    Quibbler 2012-12-07 14:22:24

    看不大懂,压力大啊

  6. waynebaby
    116.226.211.*
    链接

    waynebaby 2012-12-11 09:07:33

    那个啥 等继续的案例呢

  7. 老赵
    admin
    链接

    老赵 2012-12-11 10:03:19

    @waynebaby

    等等,懒……

  8. hooyes
    114.112.45.*
    链接

    hooyes 2012-12-16 09:27:04

    深入研究,学习中。

  9. 链接

    石头 2012-12-19 11:31:09

    最喜欢.Net的泛型,它有C++模版重用代码的精妙,同时又能保持完整的编码提示。 “不同泛型参数区分独立类型”这一点我想了两年才搞明白,都怪我基础不扎实。

  10. darklx
    114.89.37.*
    链接

    darklx 2012-12-22 17:26:08

    一看第一句就知道是来打击JAVA的。

  11. 链接

    yamatamain 2012-12-28 11:24:07

    赵姐夫,有一个很棘手的问题想请教下。 WCF中使用net.msmq把内容先放到MSMQ中,然后开服务去接收这些消息。 由于接收速度无法控制,所以每当MSMQ中积累很多消息时: 有太多的Task等待处理。从而把服务器的内存占用完了。 请问你有什么好的解决方式吗? 一定要回复,拜谢。

  12. 老赵
    admin
    链接

    老赵 2012-12-29 02:27:48

    @yamatamain

    MSMQ又不是吃内存的,它是持久化的,没明白为什么会把内存占完。

  13. 链接

    yamatamain 2012-12-29 09:24:52

    貌似我表达不清楚。Task把MSMQ中的内容一次性全部读取出来,放到内存中。因为接受速度>写入数据库速度,所以会堵塞,到一定程序会让服务停止。

    所以是否可以控制Task的数量以及读取的速度。现在我使用的是如下一套机制,请问如果换成Task如何实现。

    ThreadPool.SetMaxThreads(20, 20);
    ThreadPool.SetMinThreads(20, 20);
    
    int workerThreads, completionPortThreads;
    ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads);
    
    while (workerThreads == 0)
    {
        Thread.Sleep(90);
        ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads);
    }
    
    ThreadPool.QueueUserWorkItem(o =>
    {
        AddSubjectEntity((object)entity);
    });
    
  14. 老赵
    admin
    链接

    老赵 2012-12-29 10:11:08

    @yamatamain

    Task是你程序创建的,读取是由你进行的,你当然可以控制。你每次都把MSMQ的数据都读出来,那么用MSMQ的意义是什么?为什么不能读几个处理几个,处理完了再读后面几个?消息队列哪有你这么用的。

已自动隐藏某些不合适的评论内容(主题无关,争吵谩骂,装疯卖傻等等),如需阅读,请准备好眼药水并点此登陆后查看(如登陆后仍无法浏览请留言告知)。

发表回复

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

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

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

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

使用Live Messenger联系我