开头很简单,最难的是坚持。|

陈侠云

园龄:2年10个月粉丝:1关注:1

《NET CLR via C#》---第十二章(泛型)

泛型(generic)是CLR和编程语言提供的一种特殊机制,它支持另一种形式的代码重用,即“算法重用”。

简单来说,开发人员先定义好算法,必然排序、搜索、交换、比较或者转换等。但是,定义算法的开发人员并不设定该算法要操作什么数据类型;该算法可广泛地应用于不同类型的对象。

泛型为开发人员提供了以下优势:

  • 源代码保护:使用泛型算法的开发人员不需要访问算法的源代码。然后,使用C++模板的泛型技术时,算法的源代码必须提供给准备使用算法的用户。
  • 类型安全:将泛型算法应用于一个具体的类型时,编译器和CLR能理解开发人员的意图,并保证只有与指定数据类型兼容的对象才能用于算法。
  • 更清晰的代码:由于编译器强制类型安全性,所以减少了源代码中必须进行的强制类型转换次数,使代码更容易编写和维护。
  • 更佳的性能:没有泛型的时候,要想定义常规化的算法,它的所有成员都要定义成操作Object数据类型。要用这个算法来操作值类型的实例,CLR必须在调用算法的成员之前对值类型实例进行装箱。

为了理解性能优化,我们可以通过如下的代码进行测试:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;

class Program
{
    static void Main(string[] args)
    {
        ValueTypePerfTest();
        ReferenceTypePerfTest();
    }

    private static void ValueTypePerfTest()
    {
        const int count = 100000000;

        using(new OperationTimer("List<int>"))
        {
            List<int> l = new List<int>();
            for (int i = 0; i < count; i++)
            {
                l.Add(i);
                int x = l[i];
            }
            l = null;
        }

        using (new OperationTimer("ArrayList of int"))
        {
            ArrayList l = new ArrayList();
            for (int i = 0; i < count; i++)
            {
                l.Add(i);           // 装箱
                int x = (int)l[i];  // 拆箱
            }
            l = null;
        }
    }

    private static void ReferenceTypePerfTest()
    {
        const int count = 100000000;

        using (new OperationTimer("List<string>"))
        {
            List<string> l = new List<string>();
            for (int i = 0; i < count; i++)
            {
                l.Add("X");
                string x = l[i];
            }
            l = null;
        }

        using (new OperationTimer("ArrayList of string"))
        {
            ArrayList l = new ArrayList();
            for (int i = 0; i < count; i++)
            {
                l.Add("X");          
                string x = (string)l[i];  
            }
            l = null;
        }
    }
}

internal sealed class OperationTimer : IDisposable
{
    private Stopwatch stopwatch;
    private string text;
    private int collectionCollect;

    public OperationTimer(string text)
    {
        PrepareForOperation();
        this.text = text;
        collectionCollect = GC.CollectionCount(0);

        stopwatch = Stopwatch.StartNew();
    }

    public void Dispose()
    {
        Console.WriteLine($"{stopwatch.Elapsed} (GCs={GC.CollectionCount(0) - collectionCollect}) {text}");
    }

    private static void PrepareForOperation()
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
    }
}

最后会得到如下输出:
image
很明显,在操作值类型时,泛型算法比非泛型算法快了近乎11倍了。此外用ArrayList操作值类型,会造成大量装箱,最终要进行293次垃圾回收。

不过,引用类型,差异则没有那么明显了,GC一样都是0,时间虽然泛型略快一点,但也不像值类型有这么大的差距。

开放类型和封闭类型

具有泛型类型参数的类型仍然是类型,CLR同样会为它创建内部的类型对象。这一点适合引用类型,值类型,接口类型和委托类型。然而,具有泛型类型参数的类型称为开放类型,CLR禁止构造开放开放类型的任何实例。

代码引用泛型类型时可指定一组泛型类型参数。为所有类型参数都传递了实际的数据类型,类型就称为封闭类型。CLR允许构造封闭类型的实例。例如以下例子中,我分别尝试用反射的方法去实例化一个开放类型和封闭类型:

using System;
using System.Collections.Generic;

class Program
{
    static void Main(string[] args)
    {
        try
        {
            Type t1 = typeof(Dictionary<,>);
            var o1 = Activator.CreateInstance(t1);
            Console.WriteLine($"{t1.ToString()}实例化传功");
        }
        catch(ArgumentException e)
        {
            Console.WriteLine(e);
        }

        try
        {
            Type t2 = typeof(Dictionary<int, int>);
            var o2 = Activator.CreateInstance(t2);
            Console.WriteLine($"{t2.ToString()}实例化传功");
        }
        catch(ArgumentException e)
        {
            Console.WriteLine(e);
        }
    }
}

--------输出结果------
ystem.ArgumentException: Cannot create an instance of System.Collections.Generic.Dictionary`2[TKey,TValue] because Type.ContainsGenericParameters is true.
   at System.RuntimeType.CreateInstanceCheckThis()
   at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, Boolean wrapExceptions)
   at System.Activator.CreateInstance(Type type, Boolean nonPublic, Boolean wrapExceptions)
   at System.Activator.CreateInstance(Type type)
   at Program.Main(String[] args) in C:\Users\LH89\source\repos\ConsoleApp3\Program.cs:line 13
System.Collections.Generic.Dictionary`2[System.Int32,System.Int32]实例化传功

可以看出,只有封闭类型才能实例化成功。从输出类型可以看出,类型名以“`”字符和一个数字结尾。数字代表类型的元数,也就是类型要求的类型参数个数。

还要注意,CLR会在类型对象内部分配类型的静态字段。因此,每个封闭类型都有自己的静态字段。换言之,假如List<T>定义了任何静态字段,则List<int>和List<string>不会共享这些静态字段。另外,假如泛型类型定义了静态构造器,那么针对每个封闭类型,这个构造器都会执行一次。

泛型类型和继承

泛型类型仍然是类型,所以能从其他任何类型派生。使用泛型类型并指定类型实参时,实际是在CLR中定义了一个新的类型对象,新的类型对象从泛型类型派生自的那个类型派生。

class Program
{
    static void Main(string[] args)
    {
        TypeNode<int> a = new TypeNode<int>();
        TypeNode<string> b = new TypeNode<string>();

        Node start = new Node();
        start.next = a;
        start.next.next = b;
    }
}

public class Node
{
    public Node next;
}

public class TypeNode<T> : Node { }

例如上例中,Node<int>和Node<string>都继承Node基类,我们也可以利用到多态的特点,将值类型和引用类型装入同一个链表中,同时避免了值类型装箱拆箱的特点。

同一性

为了对语法进行增强,有的开发人员定义了一个新的非泛型类类型,它从一个泛型类型派生,并指定了所有类型实参,例如:

public class DateTimeList : List<DateTime> { }

此时就可以简化创造列表代码:

List<DateTime> list1 = new List<DateTime>(); ->
DateTimeList list2 = new DateTimeList();

这样做表面上是简化了代码书写,但其实不妥!绝对不要出于增强源码可读性的目的来定义一个新类。这样会散失同一性(identity)和相等性(equivalence),例如我们此时比较 list1.GetType() == list2.GetType(),会返回一个false,因为比较的是不同类型的两个对象。这也意味着如果方法的原型接受一个DateTimeList,我们无法把List<DateTime>传给他,这会导致开发非常混乱。

C#也考虑到了泛型的书写困难,所以他提供了简化的语法来引用泛型封闭类型,例如我们可以在源文件顶部这样声明,就不会丧失同一性也能保证代码可读性:

using DateTimeList = System.Collections.Generic.List<System.DateTime>;

代码爆炸

使用泛型类型参数的方法在进行JIT编译时,CLR获取方法的IL,用指定的类型实参替换,然后创建恰当的本机代码。这样有个缺点:CLR要为每种不同的方法/类型组合生成本机代码,这种现象称为代码爆炸。它会使得应用程序的工作集显著增大,从而损害性能。

幸好,CLR采用了一些优化措施缓解了代码爆炸:

  1. 假如为特定的类型实参调用了一个方法,以后再用相同的类型实参调用这个方法,CLR只会为这个方法/类型组合编译一次代码。
  2. CLR认为所有引用类型实参都完全相同,所以代码能够共享。例如,CLR为List<String>的方法编译的代码可直接用于List<Stream>的方法,因为String和Stream均为引用类型。CLR之所以能执行这个优化,是因为所有引用类型的实参或变量实际只是指向堆上对象的指针,而所有对象指针都以相同的方式操纵。
    但如果类型实参是值类型,CLR就必须专门为那个值类型生成本机代码。这是因为值类型的大小不定。即使2个值类型大小一样(比如int32和uint32,都是32位),CLR仍然无法共享代码,因为可能要用不同的本机CPU指令来操纵这些值。

泛型接口

没有泛型接口,每次用非泛型接口(入IComparable)来操纵值类型都会发生装箱,而且会失去编译时的类型安全性。因此,CLR提供了对泛型接口的支持,例如:

public interface IAnimal<T>
{
    T animal { get; }
}

public class Dog : IAnimal<Dog>
{
    public Dog animal => new Dog();
}

public class Number : IAnimal<int>
{
    public int animal => 0;
}

泛型委托

CLR支持泛型委托,目的是保证任何类型的对象都能以类型安全的方式传给回调方法。此外,泛型委托允许值类型实例在传给回调方法时不进行任何装箱。
具体例子先暂时跳过,看完17章泛型再来补充

泛型方法

泛型方法的存在,为开发人员提供了极大的灵活性。例如:

    private void Swap<T>(ref T o1, ref T o2)
    {
        T temp = o1;
        o1 = o2;
        o2 = temp;
    }

有一点要注意的是,作为out/ref实参传递的变量必须具有与方法参数相同的类型,以防止损坏类型安全性。

可验证性和约束

约束的作用是限制能指定成泛型实参的类型数量。通过限制类型的数量,可以对那些类型指向更多操作。例如:

    public static T Min<T>(T o1, T o2) where T : IComparable<T>
    {
        if(o1.CompareTo(o2) < 0)
        {
            return o1;
        }

        return o2;
    }

C#的where关键字告诉编译器,为T指定的任何类型都必须实现同类型(T)的泛型IComparable接口。有了这个约束,就可以在方法中调用CompareTo,因为已知IComparable<T>接口定义了CompareTo。

约束可应用于泛型类型的类型参数,也可应用于泛型方法的类型参数。CLR不允许基于类型参数名称或约束来进行重载;只能基于元数(类型参数个数)对类型或方法进行重载。

重写虚泛型方法时,重写的方法必须指定相同数量的类型参数,而且这些类型参数会继承在基类方法指定的约束上,事实上,根本不允许为重写的方法的类型参数指定任何约束。单类型参数的名称是可以改变的。

主要约束

类型参数可以指定零个或者一个主要约束。主要约束可以是代表非密封类的一个引用类型。不能指定以下特殊引用类型:System.Object,System.Array,System.Delegate,System.MulticastDelegate,System.ValueType,System.Enum或者System.Void。

指定引用类型约束时,相当于向编译器承诺:一个指定的类型实参要么是与约束类型相同的类型,要么是从约束类型派生的类型。例如:

    public static T Min<T>(T o1, T o2) where T : List<int>
    {
        return o1.Count < o2.Count ? o1 : o2;
    }

有两个特殊的主要约束:class和struct。
class约束:向编译器承诺类型实参是引用类型。(任何类类型、接口类型、委托类型或者数组类型都满足这个约束)
struct约束:向编译器承诺类型实参是值类型。(包括枚举在内的任何值类型都满足这个约束,但编译器和CLR将任何System.Nullable<T>值类型视为特殊类型,不满足这个struct约束)

原因是Nullable<T>类型将它的类型参数约束为struct,而CLR希望禁止像Nullable<Nullable<T>>这样的递归类型。

次要约束

类型参数可以指定零个或者多个次要约束,次要约束代表接口类型。这种约束向编译器承诺,类型实参实现了接口,由于能指定多个接口约束,所以类型实参必须实现了所有接口约束。

还有一种次要约束称为类型参数约束,有时也称为裸类型约束。它允许一个泛型类型或方法规定:指定的类型实参要么是约束的类型,要么是约束的类型的派生类。例如:

    private static List<TBase> ConvertIList<T, TBase>(IList<T> list) where T : TBase
    {
        List<TBase> baseList = new List<TBase>(list.Count);
        foreach(var item in list)
        {
            baseList.Add(item);
        }
        return baseList;
    }

构造器约束

类型参数可指定零个或一个构造器约束,它向编译器承诺类型实参是实现了公共无参构造器的非抽象类型。例如:

    private class ConstructorConstaint<T> where T : new() 
    {
        public static T Factory()
        {
            return new T();
        }
    }

本文作者:陈侠云

本文链接:https://www.cnblogs.com/chenxiayun/p/18396580

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   陈侠云  阅读(19)  评论(0编辑  收藏  举报
//雪花飘落效果
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起