遇到问题应该多思考一下——由一个泛型方法想到的
今天在群里,有一个同学发上来了一段代码,说是从书上看到的例子,但是编译不了(有些书的作者真是误人子弟),希望帮忙找一下错在哪里,该怎么改,代码如下:
public class SortHelper { public void BubbleSort<T>(T[] array) { if (array==null) { throw new ArgumentNullException("array"); } T temp = default(T); for (int i = 0; i < array.Length; i++) { for (int j = i+1 ; j < array.Length; j++) {if (array[i]>=array[j])
{ temp = array[i]; array[i] = array[j]; array[j] = temp; } } } } }
很明显,他是想要写一个泛型的冒泡排序方法,可是这段代码是编译通不过的,编译器报的错误是“ Operator '>=' cannot be applied to operands of type 'T' and 'T' ”。看来问题出在代码中标红的if的判断表达式那里,这里涉及到了两个泛型变量的比较问题。看来使用>=来对两个T类型的变量进行比较是行不通的,那么在排序方法里该怎么比较两个元素的大小呢?
在遇到问题的时候,我们应该自己先思考一下问题出在哪里,该怎么解决,如果毫无头绪的话,再寻求帮助,不要想直接得到答案,通过思考,能有更深刻的理解。
对于这个泛型排序方法,我们来思考一下,我们想要对传入的T[]数组进行排序,但是不要忽略了排序的前提条件,就是数组T[]中的元素必须是可以比较大小的,如果数组T[]中的元素根本无法比较大小,排序也就成为了空中楼阁。所以,这里对于类型参数T,我们应该加以限制,让其实可以与同类型对象进行比较,要对T加上这个限制该怎么做呢?自然是使用类约束,我想大多数人都想到了.net基类库中为我们提供的IComparable<T>接口,关于这个接口,MSDN里说的很清楚,这里不再赘述。于是上面的代码被进行了小小的改造,如下:
public class SortHelper { public static void BubbleSort<T>(T[] array) where T:IComparable<T> { if (array==null) { throw new ArgumentNullException("array"); } T temp = default(T); for (int i = 0; i < array.Length; i++) { for (int j = i+1 ; j < array.Length; j++) {if (array[i].CompareTo(array[j])>=0)
{ temp = array[i]; array[i] = array[j]; array[j] = temp; } } } } }
我们给泛型的类型参数加上了约束,约束其必须实现IComparable<T>接口,这样在方法内部,我们就可以调用CompareTo(T)方法来对两个元素进行比较,我们使用如下代码测试一下
class Program { static void Main(string[] args) {
//int,double,float,string,byte等基元类型都是实现了IComparable接口的 int[] arrayInt=new int[]{4,2,8,12,6,90,1}; string[] arrayStr=new string[]{"abc","abcd","123","12","milk","hello"}; SortHelper.BubbleSort<int>(arrayInt); SortHelper.BubbleSort<string>(arrayStr); OutPut(arrayInt); Console.WriteLine(); OutPut(arrayStr); } static void OutPut<T>(T[] array) { foreach (T element in array) { Console.Write(element); Console.Write(" "); } } }
程序运行输出如下
看起来好像很完美,但是实际上这种方式存在2个问题:
1、由于约束了T必须实现接口IComparable<T>,所以我们无法为没有实现该接口的对象进行排序,这一点使这个泛型排序方法的适用范围大大缩小了,而我们写泛型方法的初衷就是算法的重用,希望使其对尽可能多的类型都可以使用这个方法来进行排序。
2、由于要求类型T实现IComparable<T>接口,该接口中的方法也只能被实现一次,但是我们在对一个类型进行排序的时候,可能会按照种规则来进行排序,比如对象有A和B两个属性,有时候希望按照A属性的排序规则来排序,有时候希望按照B属性的排序规则来排序,这时候上面的泛型排序方法就显得不够灵活了.
我们得继续思考了,如何能够解决这两个问题呢?如何能够让没有实现IComparable接口的对象也可以使用这个方法进行排序,让这个方法成为一个万能方法,并且可以适用于不同的排序规则。要对一个类型的多个对象进行排序,上面说过了,要保证这个类型的两个对象之间是可以比较大小的,其实这个泛型排序方法真正需要的并不要求T的两个变量之间可以比较大小,排序方法需要获得的实际上是比较大小的规则。使用泛型类型约束的手段,实际上是把比较大小的规则集成到了要比较的对象中,这一点造成了这二个问题,所以解决这二个问题的关键就是把比较大小的规则从对象中移除,单独传入到排序方法中,我想大家应该都想到该怎么做了,就是给泛型方法BubbleSort<T>增加一个参数,用于接收比较大小的规则。在这里小小骄傲一下,如果使用java的话,这个参数的类型将不得不是一个接口,如果是接口的话,我们将不得不写一个类来实现这个接口,封装比较大小规则的方法,好麻烦呀。好在微软为我们提供了委托,不仅提供了委托,后来又有了lambda表达式,说实话,我真是爱死委托了。所以终级改造版的代码是这个样子的:
public class SortHelper { //参数param1GreaterThanParam2用于判断第一个参数是不是比第二个参数大 public static void BubbleSort<T>(T[] array,Func<T,T,bool> param1GreaterThanParam2) { if (array==null) { throw new ArgumentNullException("array"); } T temp = default(T); for (int i = 0; i < array.Length; i++) { for (int j = i+1 ; j < array.Length; j++) { if (param1GreaterThanParam2(array[i],array[j])) { temp = array[i]; array[i] = array[j]; array[j] = temp; } } } } }
测试代码如下,为了测试,新增了一个类Person,假设Person类存在于第三方提供的一个dll中,有Name(string类型)和Age(int类型)两个属性
class Program { static void Main(string[] args) { int[] arrayInt=new int[]{4,2,8,12,6,90,1}; string[] arrayStr=new string[]{"abc","abcd","123","12","milk","hello"}; SortHelper.BubbleSort<int>(arrayInt, (x, y) => { return x > y; }); SortHelper.BubbleSort<string>(arrayStr, (x, y) => { return x.CompareTo(y)>0; }); Console.WriteLine("对int[]数组进行排序"); OutPut(arrayInt); Console.WriteLine(); Console.WriteLine("对string[]数组进行排序"); OutPut(arrayStr); Console.WriteLine(); Person[] persons=new Person[] { new Person(){Age=10,Name = "Tom"}, new Person(){Age = 6,Name = "Jerry"}, new Person(){Age = 15,Name = "Lucy"}, new Person() {Age = 21,Name = "Lana"}, new Person(){Age=1,Name="Baby"} }; SortHelper.BubbleSort<Person>(persons, (x, y) => { return x.Age > y.Age; }); Console.WriteLine("对数组persons按照每个人的年龄排序"); OutPut(persons,true); SortHelper.BubbleSort<Person>(persons,(x,y)=> { return x.Name.CompareTo(y.Name)>0; }); Console.WriteLine("对数组persons按照每个人的名字排序"); OutPut(persons,true); } static void OutPut<T>(T[] array,bool newLineForEach=false) { foreach (T element in array) { Console.Write(element); if (newLineForEach) { Console.WriteLine(); } else { Console.Write(" "); } } } }
测试结果如图
一个通用的泛型排序方法完成。
最后提一下泛型方法中泛型变量之间进行比较的几个准则。
如果对于泛型类型参数T未加约束,则T可以使用==操作符与null进行比较,因为如果T是值类型,则与null比较永远返回false,如果T是引用类型,与null比较是合法的。如果泛型类型参数没有约束为引用类型,则对同一个泛型类型的两个变量用==进行比较是非法的,