.One of the most criticized flaws of version 1.1 is that it does not provide support for generics. By using generics, we can greatly improve the reusability of code, and at the same time, we can also obtain strong type support, avoid implicit boxing and unboxing, and improve application performance to a certain extent. This article will systematically discuss generics for everyone. Let’s start with understanding generics.
1.1 Understanding generics
1.1.1 Why are there generics?
I think no matter how you enter the computer programming industry, you will inevitably have to face the topic of data structures and algorithms. Because it is a basic discipline of computer science, the lower the level, the higher the requirements for time efficiency and space efficiency of data structures or algorithms.
For example, when you call the Sort() method on an instance of a collection type (such as ArrayList) to sort it, the .Net framework is applied under the hood Quick sort algorithm. The name of the quick sort method in the .Net framework is QuickSort(), which is located in the Array type, which can be viewed through the Reflector.exe tool.
We are not going to discuss whether the QuickSort() implementation is good or not, whether it is efficient or not, this deviates from our topic. But I would like to ask everyone to think about a question: If you were to implement a sorting algorithm, what would you do? Okay, let’s narrow the topic a little more and implement the simplest Bubble Sort algorithm. If you have no experience using generics, I guess you may write the following without hesitation. Code, because this is the standard implementation of university tutorials:
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; } } } } }
Readers who are not familiar with bubble sorting can safely ignore the method body of the above code, it will not There is no obstacle to your understanding of generics. You only need to know the function it implements: rearrange the elements of an array in order from small to large. We conducted a small test on this program:
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(); } }
The output is:
1 3 4 7 8
We found that it worked well, and we were happy to think that this was the best solution. Until soon, we need to sort an array of byte type, and our above sorting algorithm can only accept an array of type int, although we know that they are completely compatible, because the byte type is a subset of the int type, But C# is a strongly typed language, and we cannot pass in a byte array in a place that accepts an int array type. Okay, it doesn’t matter. Now it seems that the only way is to copy the code and then change the signature of the method:
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, let’s solve it again Although I always feel that something is a little awkward, this code is already working. According to the idea of agile software development, do not abstract and respond to changes prematurely. When changes first appear, use the fastest method to solve it. , when changes appear for the second time, better architecture and design will be carried out.
The purpose of this is to avoid over-design, because it is very likely that the second change will never appear, but you spent a lot of time and effort making it A "perfect design" that will never be used.
This is very similar to a proverb, "fool me once, shame on you. Fool me twice, shame on me." The translation means "fool me once, shame on you." It's you who are bad; you fooled me twice, it's me who is stupid."
Good things never last forever. We will soon need to sort an array of char type. Of course, we can follow the approach of byte type array and continue to use copying. Paste the method and modify the signature of the method.
But unfortunately, we don’t want it to fool us twice, because no one wants to prove themselves stupid, so now it’s time to think of a better solution.
If we compare these two methods carefully, we will find that the implementation of these two methods is exactly the same. There is no difference except for the signature of the method. If you have ever developed a Web site program, you will know that for some sites with very large page views, in order to avoid overloading the server, static page generation is usually used, because using URL rewriting still consumes a lot of server resources. However, after generating an html static web page, the server only returns the file requested by the client, which can greatly reduce the load on the server.
When generating static pages on the Web, there is a commonly used method, which is the template generation method. Its specific method is: each time a static page is generated, first Load the template. The template contains some placeholders marked with special characters. Then we read the data from the database, use the read data to replace the placeholders in the template, and finally put the template on the server according to certain naming rules. Save it as a static html file.
我们发现这里的情况是类似的,我来对它进行一个类比:我们将上面的方法体视为一个模板,将它的方法签名视为一个占位符,因为它是一个占位符,所以它可以代表任何的类型,这和静态页面生成时模板的占位符可以用来代表来自数据库中的任何数据道理是一样的。接下来就是定义占位符了,我们再来审视一下这三个方法的签名:
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中文网(m.sbmmt.com)!