CLR笔记:16.泛型
泛型:支持值类型和引用类型,不支持枚举。
没有泛型属性。
泛型的好处:
源代码保护。使用泛型算法不需要访问算法的源码——相对于C++模板
类型安全——相对于ArrayList
更加清晰的源码——不需要拆箱,显示转换
更佳的性能——不用装箱。测试:循环1000万次,泛型List<T>与ArrayList分别用时0.1s和2s
16.1 FCL中的泛型
List<T> 取代ArrayList
Directory<TKey, TValue>取代HashTable
Stack<T>,Queue<T>分别取代Stack,Queue
IList,IDirectory,ICollection,IEnumerator,IEnumerable,IComparer,IComparable分别由相应的泛型接口(加上<T>)
16.3 泛型基础结构
这一节的前言很有意思:如何在已有的CLR中添加泛型:
创建新的IL泛型指令
修改元数据格式,以支持泛型参数/类型/方法
修改各种语言C#/VB.NET
修改编译器csc,使之生成新的IL泛型指令/元数据
修改JITer,使之可以处理新的IL泛型指令
创建新的反射成员:泛型参数/类型/方法
修改调试器
修改vs2005智能感知
CLR为应用程序使用的每个类型创建一个内部数据结构,称为“类型对象”。
具有泛型类型参数的一个类型class<T>,仍然是一个类型,也具有一个类型对象,称为“开放式类型”,CLR禁止构造开放式类型的实例;
当T是一个实际类型时class<Guid>,称为“封闭式类型”,CLR中可以创建封闭式类型的实例。
语法Activator.Creator(type):根据type获取其一个实例。有
typeof(Dictionary< , >) //运行期会报错,使用了“开放式类型”
又
t = typeof(Dictionary<String , String>)
Console.WriteLine(t.ToString()); //输出 System.Collections.Generic.Dictionary`2[System.String,System.String],这就是泛型Dictionary<String , String>的类型,在IL中也生成诸如Dictionary`2的方法,这里,2表示类型参数的数量。
每个封闭式类型都有自己的静态字段,也就是说,List<String>和List<Int32>具有各自的静态字段和cctor。
在泛型上定义cctor的目的:为类型参数加限制条件。例如,希望泛型只用于处理enum——解决了无法用约束控制Enum:
{
static GenericOnlyForEnum()
{
if (!typeof(T).IsEnum)
{
throw new Exception();
}
}
}
泛型类型与继承
由于List<T>从System.Object派生,所以List<String>也是从System.Object派生。
书中演示了一个例子——定义并使用一个链表节点类,很值得琢磨,原理就是利用上面这句话。
方法1:
{
public T m_data;
public Node<T> m_next;
public Node(T data) : this(data, null) { }
public Node(T data, Node<T> next)
{
m_data = data;
m_next = next;
}
}
public static class Program
{
static void Main()
{
Node<Char> head = new Node<char>('C');
head = new Node<char>('B', head);
head = new Node<char>('A', head);
}
}
评论:这个方法不灵活,只能构造同一种泛型Node<char>。
其实呢,是因为Node<T>这个类定义的太大了,可以一拆为二:Node类和TypeNode<T>派生类,其中Node类只记录next节点。而在其子类TypeNode<T>中,设置具体数据,如下方法2:
{
protected Node m_next;
public Node(Node next)
{
m_next = next;
}
}
class TypeNode<T> : Node
{
public T m_data;
public TypeNode(T data) : this(data, null) { }
public TypeNode(T data, Node next)
: base(next)
{
m_data = data;
}
}
这时候,就可以任意设置数据了,因为这次head声明为Node基类型,所以可以改变其子类:
{
Node head = new TypeNode<Char>('A');
head = new TypeNode<DateTime>(DateTime.Now, head);
}
泛型类型同一性:
using XXX = System.Collections.Generic.List<System.DateTime>;
接下来就可以使用XXX来代替List<System.DateTime>
——这只是对封闭性类型而言的,一种简写
代码爆炸:
对于泛型,CLR为每种不同的方法/类型组合生成本地代码,乘法原理——Assembly越来越大
由此,CLR有专门优化措施:
对于引用类型的类型实参,只编译一次,以后可以共享代码——因为都是指针,可以看作相同类型;对于值类型,不能共享,还是要“爆炸”的。
16.4 泛型接口
以FCL中的IEnumerator<T>为例:
{
T Current { get;}
}
有两种实现方式:
class A : IEnumerable<String>
{
private String current;
public String Current { get { return current; } }
}
//2.不指定T
class B : IEnumerable<T>
{
private T current;
public T Current { get { return current; } }
}
16.5 泛型委托
泛型委托允许值类型实例传递给回调方法时不装箱。
16.6 泛型方法
如果泛型类中有泛型方法,则各自声明的类型参数不能重名。
泛型类的ctor等于同名非泛型类的ctor
使用泛型方法和ref进行Swap操作
类型推导:自动判断泛型方法要使用的类型,如下:
可以直接放入两个Int32:
Swap(ref n1, ref n2);
但是,以下使用会在编译期报错,即使是实际引用相同的类型,但是推导是根据声明类型来判断的:
Object s2 = "s2";
Swap(ref s1, ref s2);
泛型方法重载:
{
Console.WriteLine(s);
}
static void Display<T>(T o)
{
Display(o.ToString());
}
这时,调用方法就有学问了:
Display("s1");
编译器总是优先选择更显示的匹配,于是以上语句直接调用非泛型方法;否则会陷入死循环。
16.7 泛型和其他成员
C#中,属性/索引/事件/操作符方法/ctor/finalzer不能有泛型方式。
允许有的:class/struct/interface/funcction/delegate
16.8 可验证性和限制
在泛型中,类型参数T,只能使用System.Object的方法,比如说ToString();不能使用其他任何方法,因为不知道T类型是否支持该方法。
使用where约束,从而可以使用T的更多方法:
where T:Icomparable<T> 告诉编译器,T表示的类型,必须实现相应的Icomparable<T>接口,从而T类型实例可以使用该接口的CompareTo方法。
where可以用于class和function
泛型方法重载原则:只能基于类型参数的个数进行重载,个数为0表示非泛型方法。
对于class,可以有:
class AType<T> { }
class AType<T1, T2> { }
//class AType<T1, T2> where T : IComparable<T> { }
被注释的类不可以再声明——class也是根据类型参数的个数,但是不考虑where约束的有无
对于泛型虚方法,重写的版本必须指定相同数量的参数类型,会继承基类方法的约束,但是不可以再指定任何新的约束。
3种约束:
1.主要约束:class和struct,分别约束类型参数为引用类型和值类型
{
public T Test()
{
return new T(); //只有值类型可以new T();
}
}
class ForClass<T> where T : class
{
public void Test()
{
T temp = null; //只有引用类型可以使用null
}
}
对于T:class约束,T可以是类/接口/委托/数组类型
2.次要约束:接口约束和裸类型约束
接口约束见14章笔记,这里主要讲裸类型约束。
裸类型约束,在类型参数间存在一个关系where T1:T2,这里可以写为where String:String,即兼容于自身
3.构造器约束:类型参数一定实现了public的无参构造器:where T:new()
不能同时指定构造器约束和struct约束——这是多余的,因为所有值类型都隐式实现public无参构造器
上面提到:只有值类型可以new T(); 但是,如果有了new()约束,引用类型也可以使用new T()了,因为肯定有构造器,所以new的动作总是可以执行。示例如下:
{
public static T Factory()
{
return new T();
}
}
public static class Program
{
static void Main()
{
ConstructConstraint<A>.Factory();
}
}
其他可验证问题:
1.不能显示将泛型类型变量转型,要先转为Object,再显示转换;对于值类型,要使用as操作符来控制运行期错误
{
//Int32 x = (Int32)obj; 不能这么写
//值类型,应该这么写
Int32 x = (Int32)(Object)obj;
//引用类型,最好这么写
String s = obj as String;
}
2.使用default关键字来为泛型变量设置默认值:
T temp = default(T); //null或者0
3.将泛型变量与null进行比较
对于引用类型,无论是否被约束,都可以使用==和!=比较null
对于值类型,如果未被约束,则永远为null,因为if(obj==null)永远为真,这里并不会报错;
如果被约束为struct,则不能比较null,因为if(obj==null)结果永远为真,但是这里会报错。
4.两个泛型类型变量的比较,使用==和!=
引用类型是可以比较的;
两个值类型不可以使用==比较,除非重载了==操作符。
如果未被约束,则不能比较,除非重载了==操作符,约束为引用可以比较。
5.泛型类型变量作为操作数使用
不能使用操作符(+,-=,*,<等等)操作泛型类型变量