.Net 버전 1.1의 가장 비판적인 결함 중 하나는 제네릭에 대한 지원을 제공하지 않는다는 것입니다. 제네릭을 사용함으로써 코드의 재사용성을 크게 향상시킬 수 있으며 동시에 강력한 유형 지원을 얻고 암시적 박싱 및 언박싱을 방지하며 애플리케이션 성능을 어느 정도 향상시킬 수 있습니다. 이 글에서는 모든 사람을 위한 제네릭에 대해 체계적으로 논의할 것입니다. 제네릭을 이해하는 것부터 시작하겠습니다.
1.1 제네릭의 이해
1.1.1 제네릭은 왜 존재하는가?
어떻게 컴퓨터 프로그래밍 업계에 입문하더라도 자료 구조와 알고리즘이라는 주제를 필연적으로 마주하게 될 것이라고 생각합니다. 컴퓨터 과학의 기본 학문이기 때문에 수준이 낮을수록 데이터 구조나 알고리즘의 시간 효율성과 공간 효율성에 대한 요구 사항이 높아집니다.
예를 들어 컬렉션 유형(예: ArrayList)의 인스턴스에서 Sort() 메서드를 호출하여 정렬하면 .Net 프레임워크가 다음 아래에 적용됩니다. 후드 퀵 정렬 알고리즘. .Net 프레임워크의 빠른 정렬 방법 이름은 QuickSort()이며 Array 유형에 있으며 Reflector.exe 도구를 통해 볼 수 있습니다.
QuickSort() 구현이 좋은지 아닌지, 효율적인지 아닌지에 대해서는 논의하지 않을 것입니다. 이는 우리의 주제에서 벗어납니다. 하지만 저는 여러분 모두에게 한 가지 질문에 대해 생각해 보라고 말하고 싶습니다. 정렬 알고리즘을 구현한다면 무엇을 하시겠습니까? 주제를 좀 더 좁혀서 가장 간단한 버블 정렬 알고리즘을 구현해 보겠습니다. 제네릭을 사용해 본 경험이 없다면 주저 없이 다음 코드를 작성할 수 있을 것입니다. 이것이 대학 튜토리얼의 표준 구현이기 때문입니다.
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
우리는 이 프로그램이 잘 작동한다는 것을 알았고 이것이 최선의 해결책이라고 생각하게 되어 기쁩니다. 곧 우리는 바이트 유형의 배열을 정렬해야 하며 위의 정렬 알고리즘은 int 유형의 배열만 허용할 수 있습니다. 비록 바이트 유형이 int 유형의 하위 집합이기 때문에 완전히 호환된다는 것을 알고 있지만 C# 강력한 유형의 언어이므로 int 배열 유형을 허용하는 위치에 바이트 배열을 전달할 수 없습니다. 좋아요, 상관없습니다. 이제 유일한 방법은 코드를 다시 복사한 다음 메서드의 서명을 변경하는 것 같습니다.
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; } } } } }
좋아요. 뭔가 좀 어색하다는 느낌은 항상 들지만, 이 코드는 이미 작동하고 있습니다. 애자일 소프트웨어 개발이라는 생각에 따라 변경사항이 처음 나타날 때 성급하게 추상화하고 대응하지 말고 가장 빠른 방법을 사용하세요. 이를 해결하면 두 번째 변경 사항이 나타나면 더 나은 아키텍처와 디자인이 수행됩니다.
두 번째 변경 사항은 절대 나타나지 않을 가능성이 높지만 이를 만드는 데 많은 시간과 노력을 들였기 때문에 과도한 디자인을 피하기 위한 것입니다. 결코 사용되지 않을 "완벽한 디자인".
이것은 "한 번 속이면 부끄럽다. 두 번 속으면 부끄럽다"라는 속담과 매우 유사합니다. 너한테." 나쁜 건 너야, 나를 두 번 속였어, 바보는 나야."
곧 char 유형의 배열을 정렬해야 합니다. 물론 바이트 유형 배열의 예를 따르고 계속해서 복사를 사용할 수 있습니다. . 메소드를 붙여넣고 메소드의 서명을 수정하십시오.
하지만 불행하게도 우리는 두 번 속는 것을 원하지 않습니다. 누구도 자신이 멍청하다는 것을 증명하고 싶어하지 않기 때문입니다. 이제 더 나은 해결책을 생각할 때입니다.
두 메소드를 잘 비교해 보면 두 메소드의 구현 방식은 메소드의 시그니처를 제외하면 완전히 동일하다는 것을 알 수 있습니다. 웹 사이트 프로그램을 개발한 적이 있다면 페이지 보기가 매우 큰 일부 사이트의 경우 서버 과부하를 피하기 위해 일반적으로 정적 페이지 생성이 사용된다는 것을 알고 있을 것입니다. URL 재작성을 사용하면 여전히 많은 서버 리소스가 소비되기 때문입니다. 그러나 html 정적 웹 페이지를 생성한 후 서버는 클라이언트가 요청한 파일만 반환하므로 서버의 부하를 크게 줄일 수 있습니다.
웹에서 정적 페이지를 생성할 때 흔히 사용하는 방법이 있는데, 그 구체적인 방법은 정적 페이지가 생성될 때마다, 먼저 템플릿을 로드합니다. 템플릿에는 특수 문자가 표시된 일부 자리 표시자가 포함되어 있습니다. 그런 다음 데이터베이스에서 데이터를 읽고 읽은 데이터를 사용하여 템플릿의 자리 표시자를 바꾼 다음 마지막으로 특정 명명 규칙에 따라 템플릿을 서버에 넣습니다. 정적 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必须实现IComparable接口,我们像下面这样重新定义SortHelper
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);
这样尽管它是一个泛型方法,但是在使用上与普通方法已经没有了任何区别。
1.1.4 总结
本节中我们学习了掌握泛型所需要的最基本知识,你看到了需要泛型的原因,它可以避免重复代码,还学习到了如何使用类型参数和泛型方法。拥有了本节的知识,你足以应付日常开发中的大部分场景。
以上就是C#编程中的泛型的内容,更多相关内容请关注PHP中文网(www.php.cn)!