理解泛型

1.1 理解泛型
1.1.1 为什么要有泛型?

我想不论大家通过什么方式进入了计算机程序设计这个行业,都免不了

要面对数据结构和算法这个话题。因为它是计算机科学的一门基础学科

,往往越是底层的部分,对于数据结构或者算法的时间效率和空间效率

的要求就越高。比如说,当你在一个集合类型(例如ArrayList)的实例

上调用Sort()方法对它进行排序时,.Net框架在底层就应用了快速排序

算法。.Net框架中快速排序方法名称叫QuickSort(),它位于Array类型

中,这可以通过Reflector.exe工具查看到。

我们现在并不是要讨论这个QuickSort()实现的好不好,效率高还是不高

,这偏离了我们的主题。但是我想请大家思考一个问题:如果由你来实

现一个排序算法,你会怎么做?好吧,我们把题目限定得再窄一些,我

们来实现一个最简单的冒泡排序(Bubble Sort)算法,如果你没有使用

泛型的经验,我猜测你可能会毫不犹豫地写出下面的代码来,因为这是

大学教程的标准实现:

public class SortHelper{
    public void BubbleSort(int[] array) {
        int length = array.Length;

for (int i = 0; i <= length - 2; i++) {
            for (int j = length - 1; j >= 1; j--) {

// 对两个元素进行交换
                if (array[j] < array[j - 1] ) {
                    int temp = array[j];
                    array[j] = array[j - 1];
                    array[j - 1] = temp;    
                }
            }
        }
    }
}

对冒泡排序不熟悉的读者,可以放心地忽略上面代码的方法体,它不会

对你理解泛型造成丝毫的障碍,你只要知道它所实现的功能就可以了:

将一个数组的元素按照从小到大的顺序重新排列。我们对这个程序进行

一个小小地测试:

class Program {
    static void Main(string[] args) {
        SortHelper sorter = new SortHelper();
        
        int[] array = { 8, 1, 4, 7, 3 };

sorter.BubbleSort(array);

foreach(int i in array){
            Console.Write("{0} ", i);
        }

Console.WriteLine();
        Console.ReadKey();
    }
}

输出为:

1 3 4 7 8

我们发现它工作良好,欣喜地认为这便是最好的解决方案了。直到不久

之后,我们需要对一个byte类型的数组进行排序,而我们上面的排序算

法只能接受一个int类型的数组,尽管我们知道它们是完全兼容的,因为

byte类型是int类型的一个子集,但C#是一个强类型的语言,我们无法在

一个接受int数组类型的地方传入一个byte数组。好吧,没有关系,现在

看来唯一的办法就是将代码复制一遍,然后将方法的签名改一个改了:

public class SortHelper {
    public void BubbleSort(int[] array) {
        int length = array.Length;

for (int i = 0; i <= length - 2; i++) {
            for (int j = length - 1; j >= 1; j--) {

// 对两个元素进行交换
                if (array[j] < array[j - 1]) {
                    int temp = array[j];
                    array[j] = array[j - 1];
                    array[j - 1] = temp;
                }
            }
        }
    }

public void BubbleSort(byte[] array) {
        int length = array.Length;

for (int i = 0; i <= length - 2; i++) {
            for (int j = length - 1; j >= 1; j--) {

// 对两个元素进行交换
                if (array[j] < array[j - 1]) {
                    int temp = array[j];
                    array[j] = array[j - 1];
                    array[j - 1] = temp;
                }
            }
        }
    }
}

OK,我们再一次解决了问题,尽管总觉得哪里有点别扭,但是这段代码

已经能够工作,按照敏捷软件开发的思想,不要过早地进行抽象和应对

变化,当变化第一次出现时,使用最快的方法解决它,当变化第二次出

现时,再进行更好的构架和设计。这样做的目的是为了避免过度设计,

因为很有可能第二次变化永远也不会出现,而你却花费了大量的时间精

力制造了一个永远也用不到的“完美设计”。这很像一个谚语,“fool

me once,shame on you. fool me twice, shame on me.”,翻译过

来的意思是“愚弄我一次,是你坏;愚弄我两次,是我蠢”。

美好的事情总是很难长久,我们很快需要对一个char类型的数组进行排

序,我们当然可以仿照byte类型数组的作法,继续采用复制粘贴大法,

然后修改一下方法的签名。但是很遗憾,我们不想让它愚弄我们两次,

因为谁也不想证明自己很蠢,所以现在是时候思考一个更佳的解决方案

了。

我们仔细地对比这两个方法,会发现这两个方法的实现完全一样,除了

方法的签名不同以外,没有任何的区别。如果你曾经开发过Web站点程序

,会知道对于一些浏览量非常大的站点,为了避免服务器负担过重,通

常会采用静态页面生成的方式,因为使用Url重写仍要要耗费大量的服务

器资源,但是生成为html静态网页后,服务器仅仅是返回客户端请求的

文件,能够极大的减轻服务器负担。

在Web上实现过静态页面生成时,有一种常用的方法,就是模板生成法,

它的具体作法是:每次生成静态页面时,先加载模板,模板中含有一些

用特殊字符标记的占位符,然后我们从数据库读取数据,使用读出的数

据将模板中的占位符替换掉,最后将模板按照一定的命名规则在服务器

上保存成静态的html文件。

我们发现这里的情况是类似的,我来对它进行一个类比:我们将上面的

方法体视为一个模板,将它的方法签名视为一个占位符,因为它是一个

占位符,所以它可以代表任何的类型,这和静态页面生成时模板的占位

符可以用来代表来自数据库中的任何数据道理是一样的。接下来就是定

义占位符了,我们再来审视一下这三个方法的签名:

public void BubbleSort(int[] array)
public void BubbleSort(byte[] array)
public void BubbleSort(char[] array)

会发现定义占位符的最好方式就是将int[]、byte[]、char[]用占位符替

代掉,我们管这个占位符用T[]来表示,其中T可以代表任何类型,这样

就屏蔽了三个方法签名的差异:

public void BubbleSort(T[] array) {
    int length = array.Length;

for (int i = 0; i <= length - 2; i++) {
        for (int j = length - 1; j >= 1; j--) {

// 对两个元素进行交换
            if (array[j] < array[j - 1]) {
                T temp = array[j];
                array[j] = array[j - 1];
                array[j - 1] = temp;
            }
        }
    }
}

现在看起来清爽多了,但是我们又发现了一个问题:当我们定义一个类

,而这个类需要引用它本身以外的其他类型时,我们可以定义有参数的

构造函数,然后将它需要的参数从构造函数传进来。但是在上面,我们

的参数T本身就是一个类型(类似于int、byte、char,而不是类型的实

例,比如1和‘a‘)。很显然我们无法在构造函数中传递这个T类型的数组

,因为参数都是出现在类型实例的位置,而T是类型本身,它的位置不对

。比如下面是通常的构造函数:

public SortHelper(类型 类型实例名称);

而我们期望的构造函数函数是:

public SortHelper(类型);

此时就需要使用一种特殊的语法来传递这个T占位符,不如我们定义这样

一种语法来传递吧:

public class SortHelper<T> {
    public void BubbleSort(T[] array){
        // 方法实现体
    }
}

我们在类名称的后面加了一个尖括号,使用这个尖括号来传递我们的占

位符,也就是类型参数。接下来,我们来看看如何来使用它,当我们需

要为一个int类型的数组排序时:

SortHelper<int> sorter = new SortHelper<int>();
int[] array = { 8, 1, 4, 7, 3 };
sorter.BubbleSort(array);

当我们需要为一个byte类型的数组排序时:

SortHelper<byte> sorter = new SortHelper<byte>();
byte [] array = { 8, 1, 4, 7, 3 };
sorter.BubbleSort(array);

相信你已经发觉,其实上面所做的一切实现了一个泛型类。这是泛型的

一个最典型的应用,可以看到,通过使用泛型,我们极大地减少了重复

代码,使我们的程序更加清爽,泛型类就类似于一个模板,可以在需要

时为这个模板传入任何我们需要的类型。

我们现在更专业一些,为这一节的占位符起一个正式的名称,在.Net中

,它叫做类型参数 (Type Parameter),下面一小节,我们将学习类型

参数约束。
1.1.2 类型参数约束

实际上,如果你运行一下上面的代码就会发现它连编译都通过不了,为

什么呢?考虑这样一个问题,假如我们自定义一个类型,它定义了书,

名字叫做Book,它含有两个字段:一个是int类型的Id,是书的标识符;

一个是string类型的Title,代表书的标题。因为我们这里是一个范例,

为了既能说明问题又不偏离主题,所以这个Book类型只含有这两个字段

public class Book {
    private int id;
    private string title;

public Book() { }

public Book(int id, string title) {
        this.id = id;
        this.title = title;
    }

public int Id {
        get { return id; }
        set { id = value; }
    }

public string Title {
        get { return title; }
        set { title = value; }
    }
}

现在,我们创建一个Book类型的数组,然后试着使用上一小节定义的泛

型类来对它进行排序,我想代码应该是这样子的:

Book[] bookArray = new Book[2];

Book book1 = new Book(124, ".Net之美");
Book book2 = new Book(45, "C# 3.0揭秘");

bookArray[0] = book1;
bookArray[1] = book2;

SortHelper<Book> sorter = new SortHelper<Book>();
sorter.BubbleSort(bookArray);

foreach (Book b in bookArray) {
    Console.WriteLine("Id:{0}", b.Id);
    Console.WriteLine("Title:{0}\n", b.Title);
}

可能现在你还是没有看到会有什么问题,你觉得上一节的代码很通用,

那么让我们看得再仔细一点,再看一看SortHelper类的BubbleSort()方

法的实现吧,为了避免你回头再去翻上一节的代码,我将它复制了下来

public void BubbleSort(T[] array) {
    int length = array.Length;

for (int i = 0; i <= length - 2; i++) {
        for (int j = length - 1; j >= 1; j--) {

// 对两个元素进行交换
            if (array[j] < array[j - 1]) {
                T temp = array[j];
                array[j] = array[j - 1];
                array[j - 1] = temp;
            }
        }
    }
}

尽管我们很不情愿,但是问题还是出现了,既然是排序,那么就免不了

要比较大小,大家可以看到在两个元素进行交换时进行了大小的比较,

那么现在请问:book1和book2谁比较大?小张可能说book1大,因为它的

Id是124,而book2的Id是45;而小王可能说book2大,因为它的Title是

以“C”开头的,而book1的Title是以“.”开头的(字符排序时“.”在

“C”的前面)。但是程序就无法判断了,它根本不知道要按照小张的标

准进行比较还是按照小王的标准比较。这时候我们就需要定义一个规则

进行比较。

在.Net中,实现比较的基本方法是实现IComparable接口,它有泛型版本

和非泛型两个版本,因为我们现在正在讲解泛型,为了避免“死锁”,

所以我们采用它的非泛型版本。它的定义如下:

public interface IComparable {
    int CompareTo(object obj);
}

假如我们的Book类型已经实现了这个接口,那么当向下面这样调用时:

book1.CompareTo(book2);

如果book1比book2小,返回一个小于0的整数;如果book1与book2相等

,返回0;如果book1比book2大,返回一个大于0的整数。

接下来就让我们的Book类来实现IComparable接口,此时我们又面对排序

标准的问题,说通俗点,就是用小张的标准还是小王的标准,这里就让

我们采用小张的标准,以Id为标准对Book进行排序,修改Book类,让它

实现IComparable接口:

public class Book :IComparable {
    // CODE:上面的实现略

public int CompareTo(object obj) {
        Book book2 = (Book)obj;
        return this.Id.CompareTo(book2.Id);
    }
}

为了节约篇幅,我省略了Book类上面的实现。还要注意的是我们并没有

在CompareTo()方法中去比较当前的Book实例的Id与传递进来的Book实

例的Id,而是将对它们的比较委托给了int类型,因为int类型也实现了

IComparable接口。顺便一提,大家有没有发现上面的代码存在一个问题

?因为这个CompareTo ()方法是一个很“通用”的方法,为了保证所有

的类型都能使用这个接口,所以它的参数接受了一个Object类型的参数

。因此,为了获得Book类型,我们需要在方法中进行一个向下的强制转

换。如果你熟悉面向对象编程,那么你应该想到这里违反了Liskov替换

原则,关于这个原则我这里无法进行专门的讲述,只能提一下:这个原

则要求方法内部不应该对方法所接受的参数进行向下的强制转换。为什

么呢?我们定义继承体系的目的就是为了代码通用,让基类实现通用的

职责,而让子类实现其本身的职责,当你定义了一个接受基类的方法时

,设计本身是优良的,但是当你在方法内部进行强制转换时,就破坏了

这个继承体系,因为尽管方法的签名是面向接口编程,方法的内部还是

面向实现编程。

NOTE:什么是“向下的强制转换(downcast)”?因为Object是所有类型

的基类,Book类继承自Object类,在这个金字塔状的继承体系中,

Object位于上层,Book位于下层,所以叫“向下的强制转换”。

好了,我们现在回到正题,既然我们现在已经让Book类实现了

IComparable接口,那么我们的泛型类应该可以工作了吧?不行的,因为

我们要记得:泛型类是一个模板类,它对于在执行时传递的类型参数是

一无所知的,也不会做任何猜测,我们知道Book类现在实现了

IComparable,对它进行比较很容易,但是我们的SortHelper<T>泛型类

并不知道,怎么办呢?我们需要告诉SortHelper<T>类(准确说是告诉编

译器),它所接受的T类型参数必须能够进行比较,换言之,就是实现

IComparable接口,这便是本小节的主题:泛型约束。

为了要求类型参数T必须实现IComparable接口,我们像下面这样重新定

义SortHelper<T>:

public class SortHelper<T> where T:IComparable {
    // CODE:实现略
}

上面的定义说明了类型参数T必须实现IComaprable接口,否则将无法通

过编译,从而保证了方法体可以正确地运行。因为现在T已经实现了

IComparable,而数组array中的成员是T的实例,所以当你在array[i]

后面点击小数点“.”时,VS200智能提示将会给出IComparable的成员,

也就是CompareTo()方法。我们修改BubbleSort()类,让它使用

CompareTo()方法来进行比较:

public class SortHelper<T> where T:IComparable
{
    public void BubbleSort(T[] array) {
        int length = array.Length;

for (int i = 0; i <= length - 2; i++) {
            for (int j = length - 1; j >= 1; j--) {
                
                // 对两个元素进行交换
                if (array[j].CompareTo(array[j - 1]) < 0 ) {
                    T temp = array[j];
                    array[j] = array[j - 1];
                    array[j - 1] = temp;
                }
            }
        }
    }
}

此时我们再次运行上面定义的代码,会看到下面的输出:

Id:45
Title:.Net之美

Id:124
Title:C# 3.0揭秘

除了可以约束类型参数T实现某个接口以外,还可以约束T是一个结构、T

是一个类、T拥有构造函数、T继承自某个基类等,但我觉得将这些每一

种用法都向你罗列一遍无异于浪费你的时间。所以我不在这里继续讨论

了,它们的概念是完全一样的,只是声明的语法有些差异罢了,而这点

差异,相信你可以很轻松地通过查看MSDN解决。
1.1.3 泛型方法

我们再来考虑这样一个问题:假如我们有一个很复杂的类,它执行多种

基于某一领域的科学运算,我们管这个类叫做SuperCalculator,它的定

义如下:

public class SuperCalculator {
    public int SuperAdd(int x, int y) {
        return 0;
    }

public int SuperMinus(int x, int y) {
        return 0;
    }

public string SuperSearch(string key) {
        return null;
    }

public void SuperSort(int[] array) {
    }
}

由于这个类对算法的要求非常高,.Net框架内置的快速排序算法不能满

足要求,所以我们考虑自己实现一个自己的排序算法,注意到

SuperSearch()和SuperSort()方法接受的参数类型不同,所以我们最好

定义一个泛型来解决,我们将这个算法叫做SpeedSort(),既然这个算法

如此之高效,我们不如把它定义为public的,以便其他类型可以使用,

那么按照前面两节学习的知识,代码可能类似于下面这样:

public class SuperCalculator<T> where T:IComparable {
    // CODE:略

public void SpeedSort(T[] array) {      
        // CODE:实现略
    }
}

这里穿插讲述一个关于类型设计的问题:确切的说,将SpeedSort()方法

放在SuperCaculator中是不合适的?为什么呢?因为它们的职责混淆了

,SuperCaculator的意思是“超级计算器”,那么它所包含的公开方法

都应该是与计算相关的,而SpeedSort()出现在这里显得不伦不类,当我

们发现一个方法的名称与类的名称关系不大时,就应该考虑将这个方法

抽象出去,把它放置到一个新的类中,哪怕这个类只有它一个方法。

这里只是一个演示,我们知道存在这个问题就可以了。好了,我们回到

正题,尽管现在SuperCalculator类确实可以完成我们需要的工作,但是

它的使用却变得复杂了,为什么呢?因为SpeedSort()方法污染了它,仅

仅为了能够使用SpeedSort()这一个方法,我们却不得不将类型参数T加

到SuperCalculator类上,使得即使不调用SpeedSort()方法时,创建

Calculator实例时也得接受一个类型参数。

为了解决这个问题,我们自然而然地会想到:有没有办法把类型参数T加

到方法上,而非整个类上,也就是降低T作用的范围。答案是可以的,这

便是本小节的主题:泛型方法。类似地,我们只要修改一下SpeedSort()

方法的签名就可以了,让它接受一个类型参数,此时SuperCalculator的

定义如下:

public class SuperCalculator{
    // CODE:其他实现略

public void SpeedSort<T>(T[] array) where T :

IComparable {
        // CODE:实现略
    }
}

接下来我们编写一段代码来对它进行一个测试:

Book[] bookArray = new Book[2];

Book book1 = new Book(124, "C# 3.0揭秘");
Book book2 = new Book(45, ".Net之美");

SuperCalculator calculator = new SuperCalculator();
calculator.SpeedSort<Book>(bookArray);

因为SpeedSort()方法并没有实现,所以这段代码没有任何输出,如果你

想看到输出,可以简单地把上面冒泡排序的代码贴进去,这里我就不再

演示了。这里我想说的是一个有趣的编译器能力,它可以推断出你传递

的数组类型以及它是否满足了泛型约束,所以,上面的SpeedSort()方法

也可以像下面这样调用:

calculator.SpeedSort(bookArray);

这样尽管它是一个泛型方法,但是在使用上与普通方法已经没有了任何

区别。

时间: 2024-11-06 16:26:15

理解泛型的相关文章

C#理解泛型

C# 理解泛型 出处:http://www.ondotnet.com/pub/a/dotnet/2004/05/17/liberty.html 术语表 generics:泛型 type-safe:类型安全 collection: 集合 compiler:编译器 run time:程序运行时 object: 对象 .NET library:.Net类库 value type: 值类型 box: 装箱 unbox: 拆箱 implicity: 隐式 explicity: 显式 linked list

再次理解泛型

.NET1.1版本最受诟病的一个缺陷就是没有提供对泛型的支持.在机房重构的过程中,我们也都使用了泛型,那时是初次接触,至于详细的真的是不理解?那么泛型是什么,我们先从理解泛型开始. 我们先思考这么一个问题:如果由你来实现一个冒泡排序,如果没有泛型,你会这么写: <span style="font-family:KaiTi_GB2312;font-size:18px;"> public class SortHelper { public void BubbleSort( in

Android开发之深入理解泛型extends和super的区别

摘要: 什么是泛型?什么是擦除边界?什么是上界限定或下界限定(子类型限定或超类型限定)?什么是类型安全?泛型extends关和super关键字结合通配符?使用的区别,两种泛型在实际Android开发中有什么用处? 一.什么是泛型? 泛型,大概的意思是指没有指定具体的类型的类或方法,以泛型的形式传入一个类中或方法中,在Java编程(Android编程)里面使用一对尖括号和一个大写字母表示,例如: //泛型类 public interface List<E>{} //泛型方法,类型参数位于返回类型

通过Java反射来理解泛型的本质

集合框架中经常会使用泛型指定集合中所存放元素的类型,保证集合的统一性,从集合中取出元素的时候也避免了类型强制转换的操作,所以我们使用常规的方式来往集合中存放元素的时候,如果指定泛型,那么我们只能向集合内添加泛型类型的对象,如果不指定泛型,那么可以往集合中添加任何类型的对象,因为此时默认元素是Object类的对象,取出时也需要类型强制转换,就如下面代码: 1 ArrayList list = new ArrayList(); 2 list.add(1); 3 list.add("s");

你是如何理解泛型的?

[转载自:http://blog.csdn.net/snn1410/article/details/44980467] 在没有泛型之前,一旦把一个对象"丢进"Java集合中,集合就会忘记对象的类型,把所有的对象都当成是Object类型处理. 当程序从集合中取出对象之后,就要进行强制类型转换,这种强制类型转换不仅代码臃肿,还容易引起 ClassCastException 异常. 在JDK1.5之后,Java引用了"参数化类型(parameterized type)"的

Java 理解泛型的基本含义

Java 泛型 Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型. 泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数. 泛型方法 你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数.根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用. 下面是定义泛型方法的规则: 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例

关于泛型,什么是泛型,如何理解泛型,直接上代码举例

泛型是JDK5中引入的特性,它提供了编译时类型安全检测机制,该机制允许在编译时检测到非法的类型,就不至于在运行时出现转换异常, 它的本质是参数化类型(即以任何类型 T 作为参数,<T>,在使用时可以传入类型T的具体值 t ). 也就是将类型由原来的具体类型用参数T表示(即类型参数化),然后在使用/调用时传入具体的类型.这种参数类型可以用在类.方法和接口中,分别被称 为泛型类.泛型方法.泛型接口 . 版权声明:本文为CSDN博主「timon777」的原创文章,遵循 CC 4.0 BY-SA 版权

深入理解Java之泛型

1. Why ——引入泛型机制的原因 假如我们想要实现一个String数组,并且要求它可以动态改变大小,这时我们都会想到用ArrayList来聚合String对象.然而,过了一阵,我们想要实现一个大小可以改变的Date对象数组,这时我们当然希望能够重用之前写过的那个针对String对象的ArrayList实现. 在Java 5之前,ArrayList的实现大致如下: 1 public class ArrayList { 2 public Object get(int i) { ... } 3 p

C#基础:泛型的理解和使用

日常生活中的事物都是有类型的,比如我们说“一个女人”,那么“女”就是这个人的类型.我们可以说“女人都是水做的”,那么听者都知道这是在说“女”这种类型的人.再比如你去肉店买肉,你可以对老板说“我要十斤猪肉”,那么老板一定知道你是在要“猪”这种类型的肉. 日常生活中的这些语言都是带有类型的,但是在日常生活中还有一些语言是不带类型的.比如我们经常说“人是贪婪的”,这里的人就没有类型之分,听者都知道是指所有的人;我们也可以在肉店里指着猪肉说“给我来十斤肉”,肉店老板同样知道你要的是猪肉. 程序语言必须能