17 泛型概述

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#从入门到精通》

发布了46 篇原创文章 · 获赞 53 · 访问量 3705

猜你喜欢

转载自blog.csdn.net/qq_38992372/article/details/105018793