1.object的问题
为了理解泛型,首先要理解它们用于解决什么问题。
假定要建模一个先入先出队列,可创建一个下面这样的类。
class Queue
{
private const int DEFAULTQUEUESIZE = 100;
private int[] data;
private int head = 0, tail = 0;
private int numElements = 0;
publlc Queue()
{
this.data = new int[DEFAULTQUEUESIZE] ;
}
public Queue(int size)
{
if (size > 0)
{
this.data . new int[size];
}
else
{
throw new ArgumentoutofRangeException("size", "Must be greater than zero");
}
}
//入队
public void Enqueue(int item)
{
if (this.numElements = this .data.Length)
{
throw new Exception("Queue full");
}
this. data[this.head] = item;
this,head++;
this .head %= this .data. Length;
this.numElements++;
}
//出队
public int Dequeue()
{
if (this.numElements == e)
{
throw new Exception("Queue empty");
}
int queueItem = this,data[this,tail];
this,tail++;
this.tail %= this.data. Length;
this.numElements--;
return queueItem;
}
}
Queue类能很好地支持int队列,但如果要创建字符串队列,float队列,甚至更复杂类型(比如之前讲过的Circle,或者Horse等类型)的队列又该怎么办呢?现在的问题是,Queue类的实现限定int类型的数据项。试图入队一个Horse会发生编译时错误。
Queue queue = new Queue();
Horse myHorse = new Horse();
queue. Enqueue(myHorse); //编译时错误:不能将Horse转换成int
绕开该限制的一一个办法是指定Queue类包含object类型的数据项,更新构造器,修改Enqueue和Dequeue方法来获取object参数并返回object,如下所示:
class Queue
{
...
private object[] data;
…
public Queue()
{
this.data = new object[DEFAULTQUEUESIZE];
}
publlc queue(int size)
{
…
this.data = new object[size];
…
}
public vold Enqueue(object item)
{
…
}
public object Dequeue()
{
…
object queueItem = this .data[this.tail];
…
return queueItem;
}
}
可用object类型引用任意类型的值或变量。所有引用类型都自动从.NET Framework的System.0bject类继承(无论直接还是问接)。C#的object是System.object的别名。现在,由于Enqueue和Dequeue方法操纵的是object,所以可以处理Circle、Horse、Whale或其他任何类型的队列。但必须记住将Dequeue方法的返回值转换为恰当的类型,因为编译器不自动执行从object向其他类型的转换。
Queue queue = new Queue();
Horse myHorse = new Horse();
queue . Enqueue(myHorse); //现在合法了- Horse 是object
…
Horse dequeuedHorse =(Horse)queue.Dequeue(); // 需要将object转换回Horse
如果没有对返回值进行类型转换,就会报告如下所示的编译器错误:
无法将类型从"object"隐式转换为Horse"
由于要求显式类型转换,导致object类型所提供的灵活性大打折扣。很容易写出下面
这样的代码:
Queue queue = new Queue();
Horse myHorse = new Horse();
queue.Enqueue(myHorse);
Circle myCircle = (Circle)queue ,Dequeue(); // 运行时错误
上述代码能通过编译,但运行时会抛出System. InvalidCastException异常。之所以出错,是因为代码试图将一个Horse引用存储到Circle变量中,但两种类型不兼容。这个错误只有在运行时才会显现,因为编译器在编译时没有足够多的信息来执行检查。只有运行时才能确定出队对象的实际类型。
使用object类型创建常规类和方法的另一个缺点是,如果“运行时”需要先将object转换成值类型,再从值类型转换回来,就会消耗额外的内存和处理器时间。例如,以下代码对包含int变量的队列进行操作:
Queue queue = new queue();
int myInt = 99;
queue.Enqueue(myInt);//将int装箱成object
…
myInt = (int)queue ,Dequeue();//将object拆箱成int
Queue数据类型要求它容纳的数据项是object,而object是引用类型。对值类型(例如int)进行入队操作,要求通过装箱转换成引用类型。类似地,为了出队成int,要求通过拆箱转换回值类型。这方面更多的细节请参见之前博客介绍的“装箱"和“拆箱”。虽然装箱和拆箱是透明的,但会造成性能开销,因为需进行动态内存分配。虽然对于每个数据项来说开销不大,但创建由大量值类型构成的队列时,累积起来的开销还是非常不容忽视的。
2.泛型解决方案
C#通过泛型避免强制类型转换, 增强类型安全性,减少装箱量,泛型类和方法接受类型参数。比如下面的泛型类:
class Queue<T>
{
…
}
T就是参数类型,作为占位符使用,会在编译时被真正的类型取代。
在类中定义字段和方法时,可以用同样的占位符指定这些项的类型,例如:
class Queue<T>
{
private T[] data; //数组是'T'类型。'T' 称为类型参数
...
public Queue()
{
this.data = new T[DEFAULTQUEUESIZE]; 11 T'作为数据类型
}
public Queue(int size)
{
…
this.data = new T[size];
…
}
public void Enqueue(T item) // 'T'作为方法参数类型
{
…
}
public T Dequeue() //'T' 作为返回类型
{
...
T queueItem = this.data[this.tal]; /数组中的数据是'T'类型
…
return queueItem;
}
}
2.1对比泛型类和常规类
常规类的参数能强制转换为不同的类型。
例如,前面基于object的Queue类就是常规类。该类只有一个实现,它的所有方法获取的都是object类型的参数,返回的也是object类型。可用这个类来容纳和处理int. string以及其他许多类型的值,但任何情况使用的都是同一个类的实例,必须将使用的数据转型为object,或者从object转型为正确的数据类型。
把它和泛型类Queue<T>类比较。每次为泛型类指定类型参数时(例如Queuecint>或者Queue<Horse>),实际都会造成编译器生成一个全新的类,它“恰好”具有泛型类定义的功能。这意味着Queue<int>和Queue<Horse>是全然不同的两个类型,只是“恰好”具有相同的行为。可以想象泛型类定义了一个模板,编译器根据需要用该模板来生成新的、有具体类型的类。泛型类的具体类型版本(例如Queue<int>, Queue<Horse>等)称为已构造类型(constructed type)。它们应被视为不同的类型(尽管有-一组类似的方法和属性)。
2.2泛型和约束
有时要确保泛型类使用的类型参数是提供了特定方法的类型。例如,假定要定义一一个
PrintableCollection类,就可能想确保该类存储的所有对象都提供了Print方法。这时可用约束来规定该条件。
约束限制泛型类的类型参数实现了-组特定的接口,因而提供了接口定义的方法。例如,假定IPrintable接口定义了Print方法,就可像这样定义PrintableCollection类:
public class PrintableCollection<T) where T : IPrintable
这个类编译时,编译器会验证用于替换T的类型实现了IPrintable 接口。如果没有,
就报告编译错误。
3.可变性和泛型接口
举例:
interface IWrapper<T>
{
…
}
class Wrapper<T> : IWrapper<T>
{
…
}
正确赋值:
Wrapper<string> stringWrapper = new Wrappercstring>();
IWrapper<string> storedStringWrapper = stringWrapper;
但是如果像下面这样赋值:
IWrapper<object> storedStringwrapper = stringWrapper;
该语句和前面创建Iwrapper<string>引用的语句相似,区别在于,类型参数是object而非string.该语句合法吗?记住,所有字符串都是对象(可将string 值赋给一个 object引用),所以该语句理论上可行。
但是,如果尝试执行它,会出现编译错误并显示消息:无法将类型"rapper<string>“隐式转换为"IWrapper<object>".存在一个显式转换(是否缺少强制转换?)
可以尝试显式转换:
Iwrapper<object> storedobiectWrapper = (Iwraper<object>stringWrapper;
上述代码能够编译,但在运行时会抛出InvalidCastException 异常。问题在于,虽然所有字符串都是对象,但反之不成立。
IWrapper<T>接口称为不变量(invariant)。不能将IWrapper<A>对象赋给IWrapper<B>类型的引用,即使类型A派生自类型B。 C#默认强制贯彻了这一-限制, 确保代码的类型安全性。
3.1协变接口
对于泛型接口定义的方法,如果类型参数(T)仅在方法返回值中出现,就可明确告诉编译器- 些隐式转换是合法的,没必要再强制严格的类型安全性。为此,要在声明类型参数时指定out关键字:
interface IRetrievewrapper<out T>
{
T GetData();
}
Wrapper类实现了该接口
这个功能称为协变性(Covariance).只要存在从类型A到类型B的有效转换,或者类型A派生自类型B,就可以将IRetrieveWrapper<A>对象赋给IRetrieveWrapper<B>引用。
以下代码现在能成功编译并运行:
// string 派生自object,所以现在是合法的
IRetrleveWrapper<oboject> retrievedobjectrapper = stringWrapper;
只有作为方法返回类型指定的类型参数才能使用out限定符。用类型参数指定方法的任何参数类型时,使用out限定符就是非法的,代码不会通过编译。另外,协变性只适合引用类型,因为值类型不能建立继承层次结构。
.NET Framework 定义的几个接口支持协变性,包括要在后面介绍的IEnumerable<T>接口。
5.2逆变接口
有协变性自然还有逆变性(Contravariance)。它允许使用泛型接口,通过A类型(比如String类型)的一一个引用来引用B类型(比如object类型)的一个对象, 只要A从B派生(或者说B的派生程度比A小。这听起来比较复杂,所以让我们用.NET Framework类库的一个例子来解释。
.NET Framework的System.Collections . Generic命名空间提供了名为IComparer的接口,如下所示:
public interface Icomparer<in T>
{
int Compare(T x, T y);
}
实现该接口的类必须定义Compare方法,它比较由T类型参数指定的那种类型的两个对象。Compare 方法返回一个整数值:如果x和y有相同的值,就返回日:如果x小于y,就返回负值:如果x大于y,就返回正值。以下代码展示了如何根据对象的哈希码对它们进行排序。(GetHashCode方法已由object类实现。它只是返回一个代表对象的整数。所有引用类型都继承了该方法并可用自己的实现重写。)
class ObjectComparer : IComparer<object>
{
int Comparer<object> .Compare(Object x, object y)
{
int xHash = x.GetHashCode();
int yHash = y.GetHashCode();
if (xHash = yHash)
return 8;
if (XHash < yHash)
return -1;
return 1;
}
}
可创建一个objectComparer对象,并通过IComparer<Object>接口调用Compare方法来比较两个对象,如下所示:
object x= …;
object y= …;
ObjectComparer objectComparer = new ObjectComparer();
IComparercbject> objectCorparator = objectComparer;
int result = objectComparator .Compare(x, y);
到目前为止,似乎一切再普通不过。但有趣的是,可以通过对字符串进行比较的IComparer接口来引用同-一个对象,如下所示:
IComparer<String> stringComparator = objectComparer;
表面上该语句似乎违反了类型安全性的一切规则。 然而,如果仔细考虑IComparer<T>接口所做的事情,就明白上述语句是没有问题的。Compare 方法的作用是对传入的实参进行比较,根据结果返回一个值。能比较object,自然就能比较String. String 不过是object的一种特化的类型而已。毕竞,- 一个string应该能做object能做的任何事情一这不正是继承的意义吗? !
当然,这样说仍有一点牵强。编译器怎么知道你不会在Compare方法的代码中执行依
赖于特定类型的操作,造成用基于不同类型的接口调用方法时失败?所以,必须让编译器
安心!检查IComparer接口的定义,会看到在类型参数前添加了in限定符:
in关键字明确告诉C#编译器:程序员要么传递T作为方法的参数类型,要么传递T的派生类型。程序员不能将T用作任何方法的返回类型。这样就限定了通过泛型接口引用对象时,接口要么基于T,要么基于T的派生类型。简单地说,如果类型A公开了一些操作、属性或字段,那么从A派生出类型B时,B也肯定会公开同样的操作(允许重写这些操作来提供不同的行为)、属性和字段。因此,可以安全地用类型B的对象替换类型A的对象。
协变性和逆变性在泛型世界中似乎是一个边缘化的主题,但它们实际是有用的。例如,
List<T>泛型集合类(在System. Collections .Generic命名空间中)使用IComparer<T>对象实现Sort和BinarySearch方法。一个List<ObjcD对象可包含任何类型的对象的集合,所以Sort和BinarySearch方法要求能对任何类型的对象进行排序。如果不使用逆变,Sort方法和BinarySearch方法就必须添加逻辑来判断要排序或搜索的数据项的真实类型,然后实现类型特有的排序或搜索机制。
●协变性(Covariance) 如果泛 型接口中的方法能返回字符串,它们也能返回对象。(所有字符串都是对象。)
●逆变性(Contravariance) 如果泛 型接口中的方法能获取对象参数,它们也能获取字符串参数。(对象能执行的操作字符串也能,因为所有字符串都是对象。)
参考书籍:《Visual C#从入门到精通》