重温CLR(八 ) 泛型
熟悉面向对象编程的开发人员都深谙面向对象的好处,其中一个好处是代码重用,它极大提高了开发效率。也就是说,可以派生出一个类,让他继承基类的所有能力。派生类只需要重写虚方法,或添加一些新方法,就可定制派生类的行为,使之满足开发人员的需求。泛型(generic)是clr和编程语言提供的一种特殊机制,它支持另一种形式的代码重用,即“算法重用”。
简单地说,开发人员先定义好算法,比如排序、搜索、交换、比较或者转换等。但是,定义算法的开发人员并不设定该算法要操作什么数据类型。然后,另一个开发人员只要制定了算法要操作的具体数据类型,就可以使用算法了。
大多数算法都封装在一个类型中,clr允许创建泛型引用类型和泛型值类型,但不允许创建泛型枚举类型。此外,clr还允许创建泛型接口和泛型委托。方法偶尔也封装有用的算法,所以clr允许在引用类型、值类型或接口中定义泛型方法。
泛型有两种表现形式:泛型类型和泛型方法。
泛型类型:大多数算法都封装在一个类型中,CLR允许创建泛型引用类型和泛型值类型,但不允许创建泛型枚举类型。除此之外,CLR还允许创建泛型接口和泛型委托。
泛型方法:方法偶尔也封装有用的算法,所以CLR允许引用类型、值类型或接口中定义泛型方法。
两者都是表示API的基本方法(不管是指一个泛型方法还是一个完整的泛型类型),以致平时期望出现一个普通类型的地方出现一个类型参数。比如,List<T>,在类名之后添加一个<T>,表明它操作的是一个未指定的数据类型。定义泛型类型和方法时,它为类型指定的任何变量(比如 T)都称为类型参数(type parameter)。T代表一个变量名,在源代码中能够使用一个数据类型的任何位置 ,都能使用T。
类型参数是真实类型的占位符。在泛型声明中,类型参数要放在一堆尖括号内,并以逗号分隔。所以,在Dictionary<TKey, TValue>中,类型参数是TKey和TValue。使用泛型类型或方法时,要使用真实的类型代替。这些真实的类型称为类型实参(type argument)。
泛型为开发人员提供了以下优势:
1) 源代码保护
使用一个泛型算法的开发人员不需要访问算法的源代码。然而,使用C++模板的泛型技术时,算法的源代码必须提供给准备使用算法的用户。
2) 类型安全
将一个泛型算法应用于一个具体的类型时,编译器和CLR能理解开发人员的意图,并保证只有与制定数据类型兼容的对象才能随同算法使用。
3) 更清晰的代码
由于编译器强制类型安全性,所以减少了源代码中必须进行的转型次数。
4) 更佳的性能
在有泛型之前,要想定义一个常规化的算法,它的所有成员都要定义成操作Object数据类型。这其中就要有装箱和拆箱之间的性能损失。由于现在能创建一个泛型算法来操作一个具体的值类型,所以值类型的实例能以传值的方式传递,CLR不再需要只需任何装箱操作。由于不再需要转型,所以CLR不必检查尝试一次转型操作是否类型安全,同样提高了代码的允许速度。
fcl中的泛型
泛型最明显的应用就是集合类。FCL已经定义了几个泛型集合类。其中大多数类能在Sysytem.Collections.Generic和System.Collections.ObjectModel命名空间中。要使用线程安全的泛型集合类,可以去System.Collections.Concurrent命名空间寻找。
Microsoft建议开发人员使用泛型集合类,并基于几个方面的原因,不鼓励使用非泛型集合类(arrayList之类)。首先,非泛型无法获得类型安全性、更清晰的代码和更佳的性能。其次,泛型具有更好的对象模型。
集合类实现了许多接口,放入集合中的对象也可能实现了接口,集合类可利用这些接口执行像排序这样的操作。FCL内建了许多泛型接口定义,所以在使用接口时,也能体会到泛型带来的好处。常用的接口包含在Sysytem.Collections.Generic命名空间中。
System.Array类(即所有数组的基类)提供了大量静态泛型方法,比如,AsReadonly、FindAll、Find、FindIndex等。
泛型基础结构
泛型在clr2.0中加入。为了在clr加入泛型,许多人花费了大量时间来完成这个任务,比如以下
1 创建新的il命令,使之能够识别类型实参
2 修改现有元数据表的格式,以便表示具有泛型参数的类型和方法
3 修改编程语言以支持新语法
4 修改编译器,使之能生成新的il指令和修改的元数据格式
5 创建新的反射成员,是开发人员能查询类型和成员
现在,让我们一起讨论clr内部如何处理泛型。
开放类型和封闭类型
我们讨论了clr如何为应用程序使用的各种类型创建称为类型对象(type object)的内部数据结构。具有泛型类型参数的类型仍然是类型,clr同样会为它创建内部的类型对象。这一点适合引用类型(类)、值类型(结构)、接口类型和委托类型。然而,具有泛型类型参数的类型称为开放类型,clr禁止构造开放类型的任何实例。这类似于clr禁止构造接口类型的实例。
代码引用泛型类型时可指定一组泛型类型实参。为所有类型参数都传递了实际的数据类型,类型就称为封闭类型。clr允许构造封闭类型的实例。然而,代码引用泛型类型的时候,可能留下一些泛型类型实参未指定。这会在clr中创建新的开放类型对象,而且不能创建该类型的实例。如下例子
// A partially specified open type
internal sealed class DictionaryStringKey<TValue> :
Dictionary<String, TValue>
{
}
class Program
{
private static void Main(string[] args)
{
Object o = null; // Dictionary<,> 是一个开放类型,有两个类型参数
Type t = typeof(Dictionary<,>); // 尝试创建该类型的一个实例 (失败)
o = CreateInstance(t);
Console.WriteLine(); // DictionaryStringKey<> 是一个开放类型,有一个类型参数
t = typeof(DictionaryStringKey<>); // 尝试创建该类型的一个实例 (失败)
o = CreateInstance(t);
Console.WriteLine(); // DictionaryStringKey<Guid> 是一个封闭类型
t = typeof(DictionaryStringKey<Guid>); // 尝试创建该类型的一个实例 (成功)
o = CreateInstance(t); // Prove it actually worked
Console.WriteLine("Object type=" + o.GetType()); Console.ReadKey();
} private static Object CreateInstance(Type t)
{
Object o = null;
try
{
o = Activator.CreateInstance(t);
Console.Write("已创建 {0} 的实例", t.ToString());
}
catch (ArgumentException e)
{
Console.WriteLine(e.Message);
}
return o;
}
}
还要注意的是,CLR会在类型对象内部分配类型的静态字段。因此,每个封闭类型都有自己的静态字段。换言之,假如List<T>定义了任何静态字段,这些字段不会在一个List<DataTime>和List<String>之间共享;每个封闭类型对象都有它自己的静态字段。另外,假如一个泛型类型定义了一个静态构造器,那么针对每个封闭类型,这个构造器都会执行一次。在泛型类型上定义一个静态构造器的目的是保证传递的类型参数满足特定的条件。例如,如果希望一个泛型类型值用于处理枚举类型,可以如下定义:
internal sealed calss GenericTypeThatReqiresAnEnum<T> {
static GenericTypeThatReqiresAnEnum() {
if ( !typeof (T).IsEnum) {
throw new ArgumentException("T must be an enumerated type")
}
}
}
CLR提供了一个名为"约束"(constraint)的功能,可利用它更好地定义一个泛型类型来指出哪个类型实参是有效的。
泛型类型和继承
泛型类型仍然是类型,所以它能从其他任何类型派生。使用一个泛型类型并指定类型实参时,实际上是在CLR中定义一个新的类型对象,新的类型对象是从派生该泛型类型的那个类型派生的。也就是说,由于List<T>是从Object派生的,那么List<String>和List<Guid>也是从Object派生的。
类似地,由于DictionaryStringKey<TValue>从Dictionary<String,TValue>派生,所以DictionaryStringKey<Guid>也从Dictionary<String, Guid >派生。指定类型实参不影响继承层次结构。
定义每个节点为具体数据类型的链表,比较好的办法是定义非泛型Node基类,再定义泛型TypedNode类(用node类作为基类)。这样每个节点都是一种具体的数据类型,同时获得编译时的类型安全性,并防止值类型装箱。
class Node
{
protected Node m_next;
public Node(Node next)
{
m_next = next;
}
}
internal sealed class TypedNode<T>:Node
{
public T m_data;
public TypedNode(T data):this(data,null){}
public TypedNode(T data,Node next):base(next)
{
m_data = data;
}
public override string ToString()
{
return m_data + (m_next != null ? m_next.ToString() : string.Empty);
}
}
泛型类型同一性
泛型语法有时会将开发人员弄糊涂,因为源代码中可能散布着大量“<”和” >”符号,这有损可读性。为了对语法进行增强,有的开发人员定义了一个新的非泛型类型,它从一个泛型类型派生,并制定了所有类型实参。
List<DateTime> dt = new List<DateTime>();
一些开发人员可能首先定义下面这样的一个类:
internal sealed class DateTimeList : List<DataTime> {
//这里无需放任何代码!
}
然后就可以简化创建列表的代码
DateTimeList dt = new DateTimeList ();
这样做表面上是方便了,但是绝对不要单纯出于增强源代码的易读性类这样定义一个新类。这样会丧失类型同一性(identity)和相等性(equivalence)。如下:
Boolean sameType = (typeof(List<DateTime>) == (typeof(DateTimeList));
上述代码运行时,sameType会初始化为false,因为比较的是两个不同类型的对象。也就是说,假如一个方法的原型接受一个DateTimeList,那么不能将一个List<DateTime>传给它。然而,如果方法的原型接受一个List<DateTime>,那么可以将一个DateTimeList传给它,因为DateTimeList是从List<DateTime>派生的。
幸好,C#提供一种方式,允许使用简化的语法来引用一个泛型封闭类型,同时不会影响类的相等性——使用using指令。比如:
using DateTimeList = System.Collections.Generic.List<System.DateTime>;
using指令实际定义的是名为DateTimeList的符号。
现在只想下面这行代码时,sameType会初始化为true:
Boolean sameType = (type(List<DateTime>) == (ypeof(DateTimeList));
还有,可以使用C#的隐式类型局部变量功能,让编译器根据表达式的类型来推断一个方法的局部变量的类型。
代码爆炸
使用泛型类型参数的一个方法在进行JIT编译时,CLR获取方法的IL,用指定的类型实参进行替换,然后创建恰当的本地代码。然而,这样做有一个缺点:CLR要为每种不同的方法/类型组合生成本地代码。我们将这个现象称为"代码爆炸"。它可能造成引用程序集的显著增大,从而影响性能。
CLR内建了一些优化措施,能缓解代码爆炸。首先,假如为一个特定的类型实参调用了一个方法,以后再次使用相同的类型实参来调用这个方法,CLR只会为这个方法/类型组合编译一次。所以,如果一个程序集使用List<DateTime>,一个完全不同的程序集也使用List<DateTime>,CLR只会为List<DateTime>编译一次方法。
CLR还提供了一个优化措施,它认为所有引用类型实参都是完全相同的,所以代码能够共享。例如,clr为List<String>的方法编译的代码可直接用于List<Stream>的方法,因为string和stream均为引用类型。事实上,对于任何引用类型,都会使用相同的代码。clr之所以能执行这个优化,是因为所有引用类型的实参或变量实际只是指向堆上对象的指针,而所有对象执政都以相同方式操作。
但是,假如某个类型实参是值类型,CLR就必须专门为那个值类型生成本地代码。因为值类型的大小不定。即使类型、大小相同,CLR仍然无法共享代码,可能需要用不同的本地CPU指令操作这些值
泛型接口
显然,泛型的主要作用是定义泛型的引用类型和值类型。然而,对泛型接口的支持对clr来说也很重要。没有泛型接口,每次用非泛型接口(如IComparable)来操纵值类型都会发生装箱,而且会时区编译时的类型安全性。这将严重制约泛型类型的应用方位。因此clr提供了对泛型接口的的和支持。引用类型或值类型可指定类型实参实现泛型接口。也可保持类型实参的未指定状态来实现泛型接口。
以下是泛型接口定义是FCL的一部分:
public interface IEnumerator<T> : IDisposable, IEnumerator{
T Current { get; }
}
下面的示例类型实现上述泛型接口,而且指定了类型实参。
internal sealed class Triangle : IEnumerator<Point> {
private Point[] m_Vertice;
public Point Current { get { ... } }
}
下面实现了相同的泛型接口,但保持类型实参的未指定状态:
internal sealed class ArrayEnumerator<T> : IEnumerator<T> {
private T[] m_Vertice;
public TCurrent { get { ... } }
}
注意,arrayEnumerator对象可枚举一组T对象。还要注意,current属性现在具有未指定的数据类型T。
泛型委托
CLR支持泛型委托,目的还是保证任何类型的对象都能以类型安全的方式传给回调方法。此外,泛型委托允许值类型实例在传给回调方法时不进行任何装箱。委托实际只是提供了4个方法的一个类定义。4个方法包括一个构造器、一个Invoke方法,一个BeginInvoke方法和一个EndInvoke方法。如果定义的委托类型制定了类型参数,编译器会定义委托类的方法,用指定的类型参数替换方法的参数类型和返回值类型。
例如,假定向下面这样定义一个泛型委托:
public delegate TReturn CallMe<TReturn, TKey, TValue>(TKey key, TValue value);
编译器会将它转化成一个类,该类在逻辑上可以这样表示:
public sealed class CallMe<TReturn, TKey, TValue> : MulticastDelegate {
public CallMe(Object object, IntPtr method);
public virtual TReturn Invoke(TKey key, TValue value);
public virtual IAsycResult BeginInvoke(TKey key, TValue value, AsyncCallback callback, Object object);
public virtual TReturn EndInvoke(IAsycResult result);
}
建议尽量使用在FCL中预定义的泛型Action和Func委托。
委托和接口的逆变和协变泛型类型实参
委托的每个泛型类型参数都可标记为协变量或逆变量。利用这个功能,可将泛型委托类型的变量转换为相同的委托类型(但泛型参数类型不同)。泛型类型参数可以是以下任何一种形式。
1 不变量(invariant) 意味着泛型类型参数不能更改。到目前为止,你在本质看到的全是不变量形式的泛型类型参数。
2 逆变量(contravariant) 意味着泛型类型参数可以从一个类型更改为它的某个派生类。在c#使用in 关键字标记逆变量形式的泛型类型参数。逆变量泛型类型参数只出现在输入位置,比如作为方法的参数。
3 协变量(covariant) 意味着泛型类型参数可以从一个类更改为它的某个基类,c#使用out关键字标记协变量是行的泛型类型参数。协变量泛型参数只能出现在输出位置,比如作为方法的返回类型。
例如,现在存在以下委托类型定义(它在FCL中是存在的)
public delegate TResult Func<in T, Out TResult>(T arg);
其中,泛型类型参数T用in关键字标记,这使它成为一个逆变量;泛型类型参数TResulr则用out关键字标记,这是它成为一个协变量。
所以,如果像下面这样声明一个变量:
Func<Object,ArgumenException> fn1 = null;
就可以将它转型为另一个泛型类型参数不同的Func类型:
Func<String,Exception> fn2 = fn1; //不需要显示转型
Exception e = fn("");
使用要获取泛型参数和返回值的委托时,建议尽量为逆变性和协变性指定in和out关键字。这样做不会有不良反应,并使你的委托能在更多的情形中使用。
和委托相似,具有泛型类型参数的接口也可将它的类型参数标记为逆变量和协变量。比如:
public interface IEnumerator<out T> : IEnumerator {
Boolean MoveNext();
T Current{ get; }
}
由于T是逆变量,所以以下代码可以顺利编译:
//这个方法接受任意引用类型的一个IEnumerable
Int32 Count(IEnumerable<Object> collection) { ... }
//以下调用向Count传递一个IEnumerable<String>
Int32 c = Count(new[] { "Grant" });
泛型方法
定义泛型类、结构或接口时,这些类型中定义的任何方法都可引用由类型指定的一个类型参数。类型参数可以作为方法的参数,作为方法的返回值,或者作为方法内部定义的一个局部变量来使用。然而,CLR还允许方法指定它独有的类型参数。这些类型参数可用于参数、返回值或者局部变量的类型使用。
在下面的例子中,一个类型定义了一个类型参数,一个方法则定义了它自己的专用类型参数:
internal sealed class GenericType<T> {
privete T m_value;
public GenericType(T value) { m_value = value; }
public TOutput Converter<TOutput>() {
TOutput resulr= (TOurput) Convert.ChangeType(m_value,typeof(TOutput));
return result;
}
}
在这个例子中,GenericType类定义了类型参数(T),Converter方法也定义了自己的类型参数(TOutput)。这样的GenericType可以处理任意类型。Converter方法能将m_value字段引用的对象转换成任意类型—具体取决于调用时传递的类型实参是什么。泛型方法的存在,为开发人员提供了极大的灵活性。
泛型方法的一个很好的例子是swap方法:
private static void Swap<T>(ref T o1,ref T o2){
T temp =o1;
o1=o2;
o2=temp;
}
泛型方法和类型推断
c#泛型语法因为涉及大量<>符号,所以开发人员很容易被弄得晕头转向。为了改进代码的创建,同事增强可读性和维护性,C#编译器支持在调用一个泛型方法时进行类型推断(type inference)。这意味着编译器会在调用一个泛型方法时自动判断出要使用的类型。
private static void CallingSwapUsingInference() {
Int32 n1 = , n2 = ;
Swap(ref n1, ref n2); //调用Swap<Int32>
String s1 = "A";
Object s2 = "B";
Swap(ref s1, ref s2); //错误,不能推断类型
}
执行类型推断时,C#使用变量的数据类型,而不是由变量引用的对象的实际类型。所以第二个swap调用中,c#发现s1是string,而s2是object(即使它恰好引用一个string)。由于s1和s2是不同数据类型的变量,编译器拿不准要为swap传递什么类型实参,所以会报错。
类型可定义多个方法,让其中一个方法接收具体数据类型,让另一个接收泛型类型参数,如下例所示
private static void Display(string s){
console.writeline(s)
}
private static void Display<T>(T o){
Display(o.tostring());
}
下面展示了display方法的一些调用方式
display(“jeff”); //调用Display(string s)
display(); //调用Display<T>(T o)
display<string>(“asdasd”); //调用Display<T>(T o)
在第一个调用中,编译器可调用接收string参数的display方法,也可调用泛型display方法。但c#编译器的策略是先考虑较明确的匹配,再考虑泛型匹配。对于第二个调用,编译器不能调用接收string参数的非泛型方法,所以必须调用泛型方法。
对于第三个调用,明确制定了泛型类型实参string。这告诉编译器不要尝试推断类型实参。相反,应使用显式指定的类型实参。这个例子中,编译器会假定我想调用泛型方法,所以会调用泛型方法。
泛型和其他成员
在c#中,属性、索引器、事件、操作符方法、构造器和终结器本身不能有类型参数。但它们能在泛型类型中定义,而且这些成员的代码能使用类型的类型参数。
c#之所以不允许这些成员指定自己的泛型类型参数,是因为Microsoft C#团队认为开发人员很少需要将这些成员作为泛型使用。除此之外,为这些成员添加泛型支持的代价是相当高的,因为必须为语言设计足够的语法。
可验证性和约束
编译泛型代码时,c# 会进行分析,确保代码适用于当前已有或将来可能定义的任何类型。看看下面方法
private static Boolean MethodTakingAnyType<T>(T o)
{
T temp = o;
Console.WriteLine(o.ToString());
bool b = temp.Equals(o);
return b;
}
这个方法声明了T类型的临时变量(temp)。然后,方法执行两次变量赋值和几次方法调用。这个方法适用于任何类型。无论T是引用类型,值类型或枚举类型,还是接口或委托类型,它都能工作。
在看看下面方法
public static T Min<T>(T o1, T o2)
{
if (o1.CompareTo(o2)<) return o1;
return o2;
}
min方法试图使用o1变量来调用CompareTo方法。但是,许多类型都没有提供CompareTo方法,所以c#编译器不能编译上述代码,它不能保证这个方法适用于所有类型。
所以从表面上看,使用泛型似乎做不了太多事情。只能声明泛型类型的变量,执行变量赋值,再调用Object定义的方法,如此而已!显然,加入泛型只能这么用,可以说它几乎没有任何用。幸好,编译器和clr支持称为约束的机制,可通过它使泛型变得真正有用!
约束的作用是限制能指定成泛型实参的类型数量。通过限制类型的数量,可以对那些类型执行更多操作。以下以下还是新版本的min方法,他指定了一个约束。
public static T Min<T>(T o1, T o2) where T : IComparable<T> {
if (o1.CompareTo(o2)<) return o1;
return o2;
}
c#的wheer关键字告诉编译器,为T指定的任何类型都必须实现同类型(T)的泛型IComparable接口。有了这个约束,就可以在方法中调用CompareTo,因为已知IComparable<T>接口定义了CompareTo。
现在,当代码引用泛型类型或方法时,编译器要负责保证类型实参复合指定的约束,如果不符合约束,编译器会报错。
约束可应用于一个泛型类型的类型参数,也可应用于一个泛型方法的类型参数(就像Min所展示的)。CLR不允许基于类型参数名称或约束来进行重载;只能基于元数(类型参数的个数)对类型或方法进行重载。下例对此进行了演示
internal sealed class AType { }
internal sealed class AType<T> { }
internal sealed class AType<T1, T2> { } // 错误: 与没有约束的 AType<T> 起冲突
internal sealed class AType<T> where T : IComparable<T> { } // 错误: 与 AType<T1, T2> 起冲突
internal sealed class AType<T3, T4> { } internal sealed class AnotherType
{
// 可以定义一下方法,参数个数不同:
private static void M() { }
private static void M<T>() { }
private static void M<T1, T2>() { } // 错误: 与没有约束的 M<T> 起冲突
private static void M<T>() where T : IComparable<T> { } // 错误: 与 M<T1, T2> 起冲突
private static void M<T3, T4>() { }
}
重写虚泛型方法时,重写的方法必须指定相同数量的类型参数,而且这些类型参数会继承在基类方法上指定的约束。事实上,根本不允许为重写方法的类型参数指定任何约束。但是,类型参数的名称是可以改变的。类似的,实现一个接口方法时,方法必须指定与接口方法等量的类型参数,这些类型参数将继承由接口的方法在它们前面指定的约束。下例使用虚方法演示了这一规则:
internal class Base {
public virtual void M<T1, T2>()
where T1 : struct
where T2 : class {
}
}
internal sealed class Derived : Base {
public override void M<T3, T4>()
where T3 : struct
where T4 : class
{
}
}
试图编译上述代码,编译器会报告以下错误:
error CS0460:重写和显示接口实现方法的约束是从基方法继承的,因此不能直接指定这些约束。
注释掉子类的约束,即可正常编译。下面讨论编译器\clr允许向类型参数应用的各种约束。可用一个主要约束、一个次要约束以及一个构造器约束来约束类型参数。
主要约束
类型参数可以指定零个或者一个主要约束。主要约束可以是代表未密封类的一个引用类型。不能指定以下特殊引用类型:System.Object,System.Array,System.Delagate,System.MulticastDelegate,System.ValueType,System.Enum和System.Void。
指定引用类型约束时,相当于向编译器承诺:一个指定的类型实参要么是与约束类型相同的类型,要么是从约束类型派生的类型。例如以下泛型类:
internal static class PrimaryConstraintOfStream<T> where T : Stream
{
public static void M(T stream) {
stream.Close(); // OK
}
}
在这个类定义中,类型参数t设置了主要约束Stream(在system.IO命名空间中定义)。这就告诉编译器,使用primaryConstraintOfStream的代码在指定类型实参中,必须指定stream或者从stream派生的类型。如果类型参数没有指定主要约束,就默认为system.object。
有两个特殊的主要约束:class和struct。其中,class约束向编译器承诺类型实参是引用类型。任何类类型、接口类型、委托类型或者数组类型都满足这个约束。例如以下泛型类
internal static class PrimaryConstraintOfClass<T> where T : class
{
public static void M() {
T temp = null; // 允许,T为引用类型
}
}
在这个例子中,将temp设为null是合法的,因为T已知是引用类型,而所有引用类型的变量都能设为null。不对T进行约束,上述代码就通不过编译,因为T可能是值类型,而值类型的变量不能设为null。
struct约束向编译器承诺类型实参是值类型。包括枚举在内的任何值类型都满足这个约束。但编译器和clr将任何system.nullable<T>值类型视为特殊类型,不满足这个struct约束。原因是nullable<T>类型将它的类型参数约束为struct,而clr希望禁止向nullable< nullable<T>> 这样的递归类型。
internal static class PrimaryConstraintOfStruct<T> where T : struct
{
public static T Factory() {
// 允许,因为值类型都有一个隐式无参构造器
return new T();
}
}
这个例子中的new T()是合法的,因为T已知是值类型,而所有值类型都隐式地有一个公共无参构造器。如果T不约束,约束为引用类型,或者约束为class,上述代码将无法通过编译,因为有的引用类型没有公共无参构造器。
次要约束
类型参数可以指定零个或者多个次要约束,次要约束代表接口类型。这种约束向编译器承诺类型实参实现了接口。由于能指定多个接口约束,所以类型实参必须实现了所有接口约束(以及主要约束,如果有的话)。
还有一种次要约束称为类型参数约束,有时也成为裸类型约束。这种约束用的比接口约束少得多。它允许一个泛型类型或方法规定:指定的类型实参要么就是约束的类型,要么是约束的类型的派生类。一个类型参数可以指定零个或多个类型参数约束。
private static List<TBase> ConvertIList<T, TBase>(IList<T> list)
where T : TBase
{
List<TBase> baseList = new List<TBase>(list.Count);
for (Int32 index = ; index < list.Count; index++)
{
baseList.Add(list[index]);
}
return baseList;
}
ConvertIList方法制定了两个类型参数,其中T参数由TBase类型参数约束。意味着不管为T指定书目类型实参,都必须兼容与为TBase指定的类型实参。下面这个方法演示了对ConvertIList的合法调用和非法调用:
private static void CallingConvertIList()
{
//构造并初始化一个List<String>(它实现了IList<String>)
IList<String> ls = new List<String>();
ls.Add("A String"); // 将IList<String>转换成IList<Object>
IList<Object> lo = ConvertIList<String, Object>(ls); // 将IList<String>转换成IList<IComparable>
IList<IComparable> lc = ConvertIList<String, IComparable>(ls); // 将IList<String>转换成IList<IComparable<String>>
IList<IComparable<String>> lcs =
ConvertIList<String, IComparable<String>>(ls); // 将IList<String>转换成IList<Exception>
//IList<Exception> le = ConvertIList<String, Exception>(ls); // 错误
}
构造器约束
类型参数可以指定零个或者一个构造器约束。它向编译器承诺类型实参是实现了公共无参构造器的非抽象类型。注意,如果同时使用构造器约束和struct约束,c#编译器会认为这是一个错误,因为这是多余的;所有值类型都隐式提供了公共无参构造器。
internal sealed class ConstructorConstraint<T> where T : new()
{
public static T Factory()
{
// 允许,因为值类型都有隐式无参构造器
// 而约束要求任何引用类型也要有一个无参构造器
return new T();
}
}
这个例子中的new T()是合法的,因为已知t是拥有公共无参构造器的类型。对所有值类型来说,这一点肯定成立。对于作为类型实参指定的任何引用类型,这一点也成立,因为构造器约束要求它必须成立。
开发人员有时想为类型参数指定一个构造器约束,并指定构造器要获取多个参数。目前,clr(以及c#编译器)只支持无参构造器。Microsoft认为这已经能满足几乎所有情况,我对此也表示同意。
其他可验证性问题
本节剩下部分将讨论几个特殊的代码构造。由于可验证性问题,这些代码构造在和泛型共同使用时,可能产生不可预期的行为。另外,还讨论了如何利用约束使代码重新变得可以验证。
1) 泛型类型变量的转型
将一个泛型类型的变量转型为另一个类型是非法的,除非将其转型为与一个约束兼容的类型:
private void CastingAGenericTypeVariable1<T>(T obj)
{
Int32 x = (Int32)obj; // 错误
String s = (String)obj; // 错误
}
上述两行错误是因为T可以是任何任何类型,无法保证成功。为了修改上述代码使其能通过编译,可以先转型为object:
private void CastingAGenericTypeVariable2<T>(T obj)
{
Int32 x = (Int32)(Object)obj; // 不报错
String s = (String)(Object)obj; // 不报错
}
现在虽然能编译通过,但运行时也无法保证是正确的。
转型为引用类型时还可以使用c# is或者as操作符。
2) 将一个泛型类型变量设为默认值
将泛型类型变量设为null是非法的,除非将泛型类型约束成引用类型。
private void SettingAGenericTypeVariableToNull<T>()
{
//T temp = null; // 错误, 值类型不能设置为null,可考虑使用default('T')
}
由于未对T进行约束,所以它可能是值类型,而将值类型的变量设为null是不可能的。如果T被约束成引用类型,将temp设为null就是合法的,代码能顺利编译并运行。
c#团队认为有必要允许开发人员将变量设为它的默认值,并专门为此提供了default关键字
private void SettingAGenericTypeVariableToDefaultValue<T>()
{
T temp = default(T); // 正确
}
以上代码的default关键字告诉c#编译器和clr的jit编译器,如果t是引用类型,就将temp设为null;如果是值类型,就将temp的所有位设为0.
3) 将一个泛型类型变量与null进行比较
无论泛型类型是否非约束,使用==或!=操作符将一个泛型类型变量与null进行比较都是合法的。
private void ComparingAGenericTypeVariableWithNull<T>(T obj)
{
if (obj == null) { /* 对值类型来说,永远不会执行 */ }
}
调用这个方法时,如果为类型参数传递值类型,那么jit编译器知道if语句永远都不会为true,所以不会为if测试或者大括号内的代码生成本机代码。
如果T被约束成一个struct,C#编译器会报错。值类型的变量不能与null进行比较,因为结果始终一样。
4)两个泛型类型变量相互比较
如果泛型类型参数不是一个引用类型,对同一个泛型类型的两个变量进行比较是非法的:
private void ComparingTwoGenericTypeVariables<T>(T o1, T o2)
{
//if (o1 == o2) { } // 错误
}
在这个例子中,t未进行约束。虽然两个引用类型的变量相互比较是合法的,但两个值类型的变量相互比较是非法的,除非值类型重载了==操作符。如果t被约束城class,上述代码能通过编译。
写代码比较基元类型时,c#编译器知道如何生成正确的代码。然而,对非基元值类型,c#编译器不知道如何生成代码进行比较。所以,如果ComparingTwoGenericTypeVariables方法被约束成struct,编译器会报错。
不允许将类型参数约束成具体的值类型,因为值类型隐式密封,不可能存在从值类型派生的类型。如果允许将类型参数约束成具体的值类型,那么泛型方法会被约束为只支持该具体类型,这还不如不用泛型。
5)泛型类型变量作为操作书使用
最后要注意,将操作符应用于泛型类型的操作数会出现大量问题。在基元类型的那篇文章中,我们指出c#知道如何解释应用于基元类型的操作符(比如+,-)。但不能将这些操作符应用于泛型类型的变量。编译器在编译时确定不了类型,所以不能向泛型类型的变量引用任何操作符。所以,不可能写出一个能处理任何数值数据数据类型的算法。
private T Sum<T>(T num) where T : struct {
T sum = default(T);
for (T n = default(T); n < num; n++)
sum += n;
return sum;
}
我千方百计想让这个方法通过编译。我将T约束成一个struct,而且将default(T)和sum和n初始化为0。但编译时得到以下错误:
error cs0019 运算符“<”无法应用于T和T类型的操作数
error cs0019 运算符“+=”无法应用于T和T类型的操作数
这是clr的泛型支持体系的一个严重限制,许多开发人员(尤其是科学、金融和数学领域的开发人员)对这个限制感到很失望。许多人尝试使用各种技术避开这个限制,其中包括反射、dynamic基元类型和操作符重载。但所有这些技术都会严重损害性能或者影响代码可读性。
重温CLR(八 ) 泛型的更多相关文章
- 重温CLR(十八) 运行时序列化
序列化是将对象或对象图转换成字节流的过程,反序列化是将字节流转换回对象图的过程.在对象和字节流之间转换是很有用的机制. 1 应用程序的状态(对象图)可轻松保存到磁盘文件或数据库中,并在应用程序下次运行 ...
- 重温CLR(十三) 定制特性
利用定制特性,可宣告式为自己的代码构造添加注解来实现特殊功能.定制特性允许为几乎每一个元数据表记录项定义和应用信息.这种可扩展的元数据信息能在运行时查询,从而动态改变代码的执行方式.使用各种.NET技 ...
- 重温CLR(十二) 委托
回调函数是一种非常有用的编程机制,它的存在已经有很多年了..NET通过委托来提供回调函数机制.不同于其他平台(比如非托管C++)的回调机制,委托的功能要多得多.例如,委托确保回调方法是类型安全的(这是 ...
- 重温CLR(十一) 枚举类型、位标志和数组
枚举类型 枚举类型(enumerated types)定义了一组"符号名称/值"配对.例如,以下Color类型定义了一组符号,每个符号都标识一种颜色: internal enum ...
- 重温CLR(九) 接口
对于多继承(multiple inheritance)的概念,许多程序员并不陌生,他是指一个类从两个或多个基类派生的能力.例如,假定TransmitData类的作用是发送数据,ReceiveData类 ...
- 重温CLR(七 ) 属性和事件
无参属性 许多类型都定义了能被获取或更高的状态信息.这种状态信息一般作为类型的字段成员实现.例如一下类型包含两个字段: public sealed class Employee{ public str ...
- 重温CLR(六)方法和参数
实例构造器和类(引用类型) 构造器是将类型的实例初始化为良好状态的特殊方法.构造器方法在“方法定义元数据表”中始终叫做.ctor(constructor的简称).创建引用类型的实例时,首先为实例的数据 ...
- 重温CLR(十六) CLR寄宿和AppDomain
寄宿(hosting)使任何应用程序都能利用clr的功能.特别要指出的是,它使现有应用程序至少能部分使用托管代码编写.另外,寄宿还为应用程序提供了通过编程来进行自定义和扩展的能力. 允许可扩展性意味着 ...
- 重温CLR(十五) 托管堆和垃圾回收
本章要讨论托管应用程序如何构造新对象,托管堆如何控制这些对象的生存期,以及如何回收这些对象的内存.简单地说,本章要解释clr中的垃圾回收期是如何工作的,还要解释相关的性能问题.另外,本章讨论了如何设计 ...
随机推荐
- CAS单点登录的原理
1.首先了解几个概念 1).TGC:Ticket-granting cookie,存放用户身份认证凭证的cookie,在浏览器和CAS Server间通讯时使用.2).TGT:ticket grant ...
- APP测试的要点
APP测试要点 功能性 UI界面 安装与卸载 升级 登录测试 离线测试 安全性测试 兼容性(操作系统,屏幕尺寸,分辨率,厂家) 消息推送 前后台切换 网络环境(wifi/2G/3G/4G/无网) 异常 ...
- Keepalived + Mysql 主主复制高可用
环境 系统:Centos 7.4 x64 服务:Mariadb 5.5 .Keepalived 1.3.5.6 结构 主1:192.168.1.108 主2:192.168.1.109 VIP:19 ...
- oracle 11g的卸载
oracle 11g 的卸载主要有两种方式:一种是使用Oracle Universal Installer管理工具,该工具以向导模式进行,比较简单.这里主要讲解第二种卸载数据库的方式-----使用”d ...
- iOS开发小结 - 让你的APP后台运行
最近项目有个需求需要让app在后台一直运行计时着,找了一些资料,只能用比较无耻的做法了,播放一段没有声音的音频文件,这样你的APP就不会被系统杀掉~~ 我们只需要用到<AVFoundation/ ...
- Spring容器创建过程
Spring容器的refresh() 创建刷新 1 prepareRefresh() 刷新前的预处理 1) initProPertySources() 初始化一些属性设置: 子类定义个性化的属性 ...
- Oracle imp 导入数据出现 ORA-12560
错误如下: D:\software\xfwebdb2015-05-11\autobackup>imp Import: Release 10.2.0.1.0 - Production on 星期三 ...
- spring注解方式注入bean
用注解的方式注入bean,spring的配置文件也要增加一些约束和导入注解所在的包 applicationContext.xml <?xml version="1.0" en ...
- Pandas分组(GroupBy)
任何分组(groupby)操作都涉及原始对象的以下操作之一.它们是 - 分割对象 应用一个函数 结合的结果 在许多情况下,我们将数据分成多个集合,并在每个子集上应用一些函数.在应用函数中,可以执行以下 ...
- java-四则运算二
1.实验目的:是否有乘除法,括号,多项式运算. 2.思路:利用简单的循环switch语句进行循环输出随机数 3.程序源代码: package jiajianchengchu; import java. ...