泛型(generic)是C#语言2.0和通用语言运行时(CLR)的一个新特性。泛型为.NET框架引入了类型参数(type parameters)的概念。类型参数使得设计类和方法时,不必确定一个或多个具体参数,其的具体参数可延迟到客户代码中声明、实现。泛型允许我们声明类型参数化( type-parameterized)的代码,可以用不同的类型进行实例化。也就是说,我们可以用“类型占位符”来写代码,然后在创建类的实例时指明真实的类型。这意味着使用泛型的类型参数T,写一个类MyList<T>,客户代码可以这样调用:MyList<int>, MyList<string>或 MyList<MyClass>。这避免了运行时类型转换或装箱操作的代价和风险。
泛型概述
泛型广泛用于容器(collections)和对容器操作的方法中。.NET框架2.0的类库提供一个新的命名空间System.Collections.Generic,其中包含了一些新的基于泛型的容器类。
针对早期版本的通用语言运行时和C#语言的局限,泛型提供了一个解决方案。以前类型的泛化(generalization)是靠类型与全局基类System.Object的相互转换来实现。.NET框架基础类库的ArrayList容器类,就是这种局限的一个例子。ArrayList是一个很方便的容器类,使用中无需更改就可以存储任何引用类型或值类型。但是这种便利是有代价的,这需要把任何一个加入ArrayList的引用类型或值类型都隐式地向上转换成System.Object。如果这些元素是值类型,那么当加入到列表中时,它们必须被装箱;当重新取回它们时,要拆箱。类型转换和装箱、拆箱的操作都降低了性能;在必须迭代(iterate)大容器的情况下,装箱和拆箱的影响可能十分显著。
另一个局限是缺乏编译时的类型检查,当一个ArrayList把任何类型都转换为Object,就无法在编译时预防类似这样的操作:比如,客户代码把string和int变量放在一个ArrayList中,这是不合理的。ArrayList和其他相似的类真正需要的是一种途径,能让客户代码在实例化之前指定所需的特定数据类型。这样就不需要向上类型转换为Object,而且编译器可以同时进行类型检查。换句话说,ArrayList需要一个类型参数。这正是泛型所提供的。与ArrayList相比,在客户代码中唯一增加的List<T>语法是声明和实例化中的类型参数。代码略微复杂的回报是,你创建的表不仅比ArrayList更安全,而且明显地更加快速,尤其当表中的元素是值类型的时候。
测试证明,使用泛型,比传统的靠类型与全局基类System.Object的相互转换要节省一半的时间。
C#中的泛型
C#提供了5种泛型:类、结构、接口、委托和方法。注意,前面4个是类型,而方法是成员。在泛型类型或泛型方法的定义中,类型参数是一个占位符(placeholder),通常为一个大写字母,如T。
约束
若要检查表中的一个元素,以确定它是否合法或是否可以与其他元素相比较,那么编译器必须保证:客户代码中可能出现的所有类型参数,都要支持所需调用的操作或方法。这种保证是通过在泛型类的定义中,应用一个或多个约束(constrain)而得到的。一个约束类型是一种基类约束,它通知编译器,只有这个类型的对象或从这个类型派生的对象,可被用作类型参数。一旦编译器得到这样的保证,它就允许在泛型类中调用这个类型的方法。上下文关键字where用以实现约束。
共有5种类型的约束:
约束 | 描述 |
---|---|
where T: struct | 类型参数必须为值类型。 |
where T : class | 类型参数必须为引用类型。 |
where T : new() | 类型参数必须有一个公有、无参的构造函数。当于其它约束联合使用时,new()约束必须放在最后。 |
where T : <base class name> | 类型参数必须是指定的基类型或是派生自指定的基类型。 |
where T : <interface name> | 类型参数必须是指定的接口或是指定接口的实现。可以指定多个接口约束。接口约束也可以是泛型的。 |
类型参数的约束,增加了可调用的操作和方法的数量。这些操作和方法受约束类型及其派生层次中的类型的支持。因此,设计泛型类或方法时,如果对泛型成员执行任何赋值以外的操作,或者是调用System.Object中所没有的方法,就需要在类型参数上使用约束。
泛型类
泛型类封装了不针对任何特定数据类型的操作。泛型类常用于容器类,如链表、哈希表、栈、队列、树等等。这些类中的操作,如对容器添加、删除元素,不论所存储的数据是何种类型,都执行几乎同样的操作。
对大多数情况,推荐使用.NET框架2.0类库中所提供的容器类。当创建一个简单的泛型类和声明普通类差不多,区别如下。
- 在类名之后放置一组尖括号。
- 在尖括号中用逗号分隔的占位符字符串来表示希望提供的类型。这叫做类型参数(type parameter)
- 泛型类声明的主体中使用类型参数来表示应该替代的类型
泛型方法
与其他泛型不一样,方法是成员,不是类型。泛型方法可以在泛型和非泛型类以及结构和接口中声明。
另外在扩展方法中,它也可以和泛型类结合使用。它允许我们将类中的静态方法关联到不同的泛型类上,还允许我们像调用类构造实例的实例方法一样来调用方法。
和非泛型类一样,泛型类的扩展方法:
- 必须声明为 static
- 必须是静态类的成员
- 第一个参数类型中必须有关键字this,后面是扩展的泛型类的名字。
如下代码给出了一个叫做Print的扩展方法,扩展了叫做 Holder<T>的泛型类。
static class ExtendHolder
{
public static void Print<T>(this Holder<T> h)
{
T[] vals = h.GetValues();
Console.WriteLine("{0} {1} {2}, vals[0], vals[1], vals[2]", vals[0], vals[1], vals[2]);
}
}
class Holder<T>
{
T[] Vals = new T[3];
public Holder(T v0, T v1, T v2)
{
Vals[0] = v0;
Vals[1] = v1;
Vals[2] = v2;
}
public T[] GetValues()
{
return Vals;
}
}
class Program
{
static void Main(string[] args)
{
var intHolder = new Holder<int>(3, 5, 7);
var stringHolder = new Holder<string>("a1", "b2", "b3");
intHolder.Print();
stringHolder.Print();
}
}
泛型结构
与泛型类相似,泛型结构可以有类型参数和约束。泛型结构的规则和条件与泛型类是一样的。
泛型委托
无论是在类定义内还是类定义外,委托可以定义自己的类型参数。引用泛型委托的代码可以指定类型参数来创建一个封闭构造类型,这和实例化泛型类或调用泛型方法一样。
泛型接口
不论是为泛型容器类,还是表示容器中元素的泛型类,定义接口是很有用的。把泛型接口与泛型类结合使用是更好的用法,比如用IComparable<T>而非IComparable,以避免值类型上的装箱和拆箱操作。
泛型接口允许我们编写参数和接口成员返回类型是泛型类型参数的接口。泛型接口的声明和非泛型接口的声明差不多,但是需要在接口名称之后的尖括号中放置类型参数。
泛型代码中的 default 关键字
在泛型类和泛型方法中会出现的一个问题是,如何把缺省值赋给参数化类型,此时无法预先知道以下两点:
l T将是值类型还是引用类型
l 如果T是值类型,那么T将是数值还是结构
对于一个参数化类型T的变量t,仅当T是引用类型时,t = null语句才是合法的; t = 0只对数值的有效,而对结构则不行。这个问题的解决办法是用default关键字,它对引用类型返回空,对值类型的数值型返回零。而对于结构,它将返回结构每个成员,并根据成员是值类型还是引用类型,返回零或空。
运行时中的泛型
当泛型类或泛型方法被编译为微软中间语言(MSIL)后,它所包含的元数据定义了它的类型参数。根据所给的类型参数是值类型还是引用类型,对泛型类型所用的MSIL也是不同的。
当第一次以值类型作为参数来构造一个泛型类型,运行时用所提供的参数或在MSIL中适当位置被替换的参数,来创建一个专用的泛型类型。
例如,假设你的程序代码声名一个由整型构成的栈,如:
Stack<int> stack;
此时,运行时用整型恰当地替换了它的类型参数,生成一个专用版本的栈。此后,程序代码再用到整型栈时,运行时复用已创建的专用的栈。下面的例子创建了两个整型栈的实例,它们共用一个Stack
Stack<int> stackOne = new Stack<int>();
Stack<int> stackTwo = new Stack<int>();
然而,如果由另一种值类型——如长整型或用户自定义的结构——作为参数,在代码的其他地方创建另一个栈,那么运行时会生成另一个版本的泛型类型。这次是把长整型替换到MSIL中的适当的位置。由于每个专用泛型类原本就包含值类型,因此不需要再转换。
对于引用类型,泛型的工作略有不同。当第一次用任何引用类型构造泛型类时,运行时在MSIL中创建一个专用泛型类,其中的参数被对象引用所替换。之后,每当用一个引用类型作为参数来实例化一个已构造类型时,就忽略其类型,运行时复用先前创建的专用版本的泛型类。这可能是由于所有的引用的大小都相同。
此外,当用类型参数实现一个泛型C#类时,想知道它是指类型还是引用类型,可以在运行时通过反射确定它的真实类型和它的类型参数。
协变与逆变
逆变与协变只能放在泛型接口和泛型委托的泛型参数里面,
在泛型中out修饰泛型称为协变,协变修饰返回值 ,协变的原理是把子类指向父类的关系,拿到泛型中;在泛型中in 修饰泛型称为逆变, 逆变修饰传入参数,逆变的原理是把父类指向子类的关系,拿到泛型中。
当创建泛型类型的实例时,编译器会接受泛型类型声明以及类型参数来创建构造类型。但是,大家通常会犯的一个错误就是将派生类型分配给基类型的变量。
如下:
public class Bird
{
public int Id { get; set; }
}
public class Sparrow : Bird
{
public string Name { get; set; }
}
当申明类Bird 以及 其子类Sparrow,我们可以创建其实例bird1,bird2。
Bird bird1 = new Bird();
Bird bird2 = new Sparrow();
每一个变量都有一种类型,我们可以将派生类对象的实例赋值给基类的变量,这叫做赋值兼容性。基类是Bird,有一个Sparrow类从Bird类派生。我们可以创建了一个Sparrow类型的对象,并且将它赋值给Bird类型的变量bird2。
但当创建一个具有Bird的List数组时,如下:
List<Bird> birdList1 = new List<Sparrow>();
则会出现问题。 这是为什么呢?难道赋值兼容性的原则不成立了?
不是,这个原则还是成立,但是对于这种情况不适用!问题在于尽管Sparrow是Bird的派生类但是List
如果我们通过增加out关键字改变接口声明,并通过接口来实例化List,代码就可以通过编译了,并且可以正常工作,如下:
using System.Collections;
using System.Collections.Generic;
namespace System.Collections.Generic
{
public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}
}
此时,就可以将具有Sparrow的List对象实例化为IEnumerable
IEnumerable<Bird> birdList1 = new List<Bird>();
IEnumerable<Bird> birdList2 = new List<Sparrow>();
这就是协变(convariance)。
逆变(contravariance)则相反,增加in关键字改变口声明时,
协变和逆变的意义在于避免不必要的类型转换,简化代码和提高性能。
有关可变性的其他一些重要的事项如下。
- 变化处理的是使用派生类替换基类的安全情况,反之亦然。因此变化只适用于引用类型,因为不能从值类型派生其他类型。
- 显式变化使用in和out关键字只适用于委托和接口,不适用于类、结构和方法。
- 不包括in和out关键字的委托和接口类型参数叫做不变。这些类型参数不能用于协变或逆变。