Heim > Artikel > Backend-Entwicklung > Generika in der C#-Programmierung
Einer der am meisten kritisierten Mängel der .Net-Version 1.1 ist, dass sie keine Unterstützung für Generika bietet. Durch die Verwendung von Generika können wir die Wiederverwendbarkeit von Code erheblich verbessern und gleichzeitig eine starke Typunterstützung erhalten, implizites Boxen und Unboxen vermeiden und die Anwendungsleistung bis zu einem gewissen Grad verbessern. In diesem Artikel werden Generika für jedermann systematisch besprochen. Beginnen wir mit dem Verständnis von Generika.
1.1 Generika verstehen
1.1.1 Warum gibt es Generika?
Ich denke, egal wie man in die Computerprogrammierungsbranche einsteigt, man wird sich unweigerlich mit dem Thema Datenstrukturen und Algorithmen auseinandersetzen müssen. Da es sich um eine Grunddisziplin der Informatik handelt, sind die Anforderungen an die Zeit- und Platzeffizienz von Datenstrukturen oder Algorithmen umso höher, je niedriger das Niveau ist.
Wenn Sie beispielsweise die Methode Sort() für eine Instanz eines Sammlungstyps (z. B. ArrayList) aufrufen, um sie zu sortieren, wird das .Net-Framework unter angewendet Hood Schnellsortierungsalgorithmus. Der Name der Schnellsortierungsmethode im .Net-Framework lautet QuickSort() und befindet sich im Array-Typ, der über das Tool Reflector.exe angezeigt werden kann.
Wir werden nicht diskutieren, ob die QuickSort()-Implementierung gut ist oder nicht, ob sie effizient ist oder nicht, das weicht von unserem Thema ab. Aber ich möchte jeden bitten, über eine Frage nachzudenken: Wenn Sie einen Sortieralgorithmus implementieren würden, was würden Sie tun? Okay, grenzen wir das Thema ein wenig ein und implementieren den einfachsten Bubble-Sort-Algorithmus. Wenn Sie keine Erfahrung mit Generika haben, können Sie ohne zu zögern den folgenden Code schreiben, da dies die Standardimplementierung von Universitäts-Tutorials ist
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(); } }Die Ausgabe ist:
1 3 4 7 8Wir haben festgestellt, dass es gut funktioniert, und wir sind froh, dass dies die beste Lösung ist . . Bis bald müssen wir ein Array vom Typ Byte sortieren, und unser obiger Sortieralgorithmus kann nur ein Array vom Typ int akzeptieren, obwohl wir wissen, dass sie vollständig kompatibel sind, da der Byte-Typ eine Teilmenge des Typs int ist. Aber C# ist eine stark typisierte Sprache und wir können kein Byte-Array an einer Stelle übergeben, die einen int-Array-Typ akzeptiert. Okay, es spielt keine Rolle. Es scheint, dass die einzige Möglichkeit jetzt darin besteht, den Code zu kopieren und die Signatur der Methode zu ändern:
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; } } } } }
我们发现这里的情况是类似的,我来对它进行一个类比:我们将上面的方法体视为一个模板,将它的方法签名视为一个占位符,因为它是一个占位符,所以它可以代表任何的类型,这和静态页面生成时模板的占位符可以用来代表来自数据库中的任何数据道理是一样的。接下来就是定义占位符了,我们再来审视一下这三个方法的签名:
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,对它进行比较很容易,但是我们的SortHelper8742468051c85b06f0a0af9e3e506b5c泛型类并不知道,怎么办呢?我们需要告诉SortHelper8742468051c85b06f0a0af9e3e506b5c类(准确说是告诉编译器),它所接受的T类型参数必须能够进行比较,换言之,就是实现IComparable接口,这便是本小节的主题:泛型约束。
为了要求类型参数T必须实现IComparable接口,我们像下面这样重新定义SortHelper8742468051c85b06f0a0af9e3e506b5c:
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)!