c#中的泛型
这篇文章主要来讲讲c#中的泛型,因为泛型在c#中有很重要的位置,对于写出高可读性,高性能的代码有着关键的作用。当我多次看到自己团队的代码中包含着大量的非泛型集合,隐式的装箱和拆箱操作时,我都会建议他们补一补泛型基础。
1,什么是泛型
-
- 泛型是c#2中非常重要的一个新特性,它增强了代码的可读性,将大量的安全检查从执行期转移到编译期,从而提高代码的安全性和性能。从根本上来说,泛型实现了类型和方法的参数化。
2,为什么要使用泛型,泛型解决了什么问题
我们先来看看下面的代码(代码只是为了演示泛型,没有实际的意义),看看有什么问题?
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 ArrayList array = new ArrayList(); 6 array.Add(1); 7 array.Add(2); 8 array.Add(3); 9 ArrayList resultArray = NumberSqrt(array); 10 foreach (var item in resultArray) 11 { 12 Console.WriteLine(item); 13 } 14 } 15 public static ArrayList NumberSqrt(ArrayList array) 16 { 17 ArrayList sqrtArray = new ArrayList(); 18 foreach (var item in array) 19 { 20 sqrtArray.Add(Math.Sqrt((double)(int)item)); 21 } 22 return sqrtArray; 23 24 } 25 }
-
- 首先ArrayList是一个非泛型集合,它需要的参数是object,那么我如果往集合里面装配其它的类型,在编译时也无法判断错误(比如说我装配一个字符串进去,完全是可以编译成功的)。
- 由于ArrayList需要的是一个object,那么在你将值类型装配到集合中时,会进行隐式装箱操作,在使用集合中的数据时,需要进行拆箱操作。从而影响应用程序的性能。(关于值类型装箱请参考c#中的引用类型和值类型)。
- 代码可以可读性差,完全看不明白ArrayList应该装配什么类型。
下面来看看泛型是如何解决这些问题的:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 List<double> array = new List<double>(); 6 array.Add(1); 7 array.Add(2); 8 array.Add(3); 9 List<double> resultArray = NumberSqrt(array); 10 foreach (var item in resultArray) 11 { 12 Console.WriteLine(item); 13 } 14 } 15 public static List<double> NumberSqrt(List<double> array) 16 { 17 List<double> sqrtArray = new List<double>(); 18 foreach (var item in array) 19 { 20 sqrtArray.Add(Math.Sqrt((double)(int)item)); 21 } 22 return sqrtArray; 23 24 } 25 26 }
-
- 由于List<double>只能装配double,所以当你装配其它的类型时编译器会报错,提高代码的安全性。
- 由于List<double>只能装配double类型时,这样的话就避免了编译器隐式的装箱和拆箱操作,提高应用程序的性能。
- 一眼就可以看出集合需要装配的类型,提高代码的可读性。
- NumberSqrt方法在这里没什么用去,如果你要写一个算法,把NumberSqrt改为泛型方法:NumberSqrt<T>(比如说排序算法),可以实现算法重用。
3,如何使用泛型:语法和规则
上面已经说了什么是泛型,以及为什么要用泛型,下面我们来聊聊如何使用泛型
-
- 泛型的语法:泛型主要有泛型方法(例如:static void Swap<T>(ref T lhs, ref T rhs)),泛型接口( 例如: public interface IEnumerable<out T> : IEnumerable),泛型委托( 例如: public delegate void Action<in T>(T obj);)
- 在FCL中内建了许多泛型的接口定义(常用的接口都在System.Collections.Generic),为了使泛型能够正常的工作,微软的开发人员将会做如下工作,
- 创建新的IL指令,使之能够识别类型参数。
- 改变元数据的格式,使之能够识别泛型参数和泛型方法。
- 开放类型和封闭类型:当为一个泛型类型没有指定实际的数据类型时,就称为开放类型,例如List<T>,对于任何开放类型都不能创建该类型的实例,例如下面的代码在运行是将会报错未:经处理的异常: System.ArgumentException: 无法创建 ConsoleApplication2.Demo`1[T] 的实例,因为 Type.ContainsGenericParameters 为 True。
1 public class Demo<T> { } 2 class Program 3 { 4 static void Main(string[] args) 5 { 6 object o = null; 7 Type t = typeof(Demo<>); 8 o = Activator.CreateInstance(t); 9 } 10 }
如果为泛型类型的所有类型实参传递的都是实际数据类型,类型就称为封闭类型,CLR允许构造封闭类型的实例。
-
- 泛型类型和继承:泛型类型只是一个特殊的类型,所以它可以从其他任何类型派生。比如List<T>是从object派生的,那么List<String>也是从object派生的。换句话说,类型实参的指定和继承层次结构没有任何关系-理解这一点,有助于你判断那些转型是否能够进行,哪些转型是不能进行的。
- 编译器如何解决“代码爆炸”的问题:在使用泛型类型或者方法方法时,Clr获取IL代码,用指定的类型实参进行替换。如果Clr为每种不同的类型/方法组合都生成不同的本地代码,他可能会增大应用程序的工作集,从而影响性能。我们将这种现象称为”代码爆炸“。那么编译器又是如何解决这个问题的呢?编译器认为所有引用类型的实参是完全相同的,所以代码能够共享(为什么这么说呢?是因为所有引用类型的实参或者变量实际只是指向堆上的对象的指针,而对象的指针全部是以相同的方式来操操纵的)。但是如果某个类型实参是值类型的话,CLR就得专门为那个值类型生成本地代码,这是值类型的大小不定。
- 泛型接口:为什么要使用泛型接口呢?因为你每次试图使用一个非泛型接口来操作一个值类型时,都会发生装箱,从而失去编译时的类型安全性。所以CLR提供了对泛型接口的支持。下面来看一个例子:
1 // 摘要: 2 // 支持在泛型集合上进行简单迭代。 3 // 4 // 类型参数: 5 // T: 6 // 要枚举的对象的类型。 7 public interface IEnumerator<out T> : IDisposable, IEnumerator 8 { 9 // 摘要: 10 // 获取集合中位于枚举数当前位置的元素。 11 // 12 // 返回结果: 13 // 集合中位于枚举数当前位置的元素。 14 T Current { get; } 15 }
- 泛型委托:CLR之所以要支持泛型委托,主要目的是保证任何类型的对象都能以一种类型安全的方式传给一个回调方法,并且保证了将一个值类型实例传递给一个回调方法时不执行任何装箱处理。
- 泛型方法:定义泛型类,结构或者接口时,这些类型中定义的任何方法都可引用由类型指定的一个类型参数。类型参数可以作为方法的参数/返回值,或者作为方法内部的一个局部变量来使用。下面来看一个例子:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 MyClass<string> myclass = new MyClass<string>(); 6 myclass.ShowInfo("myClass"); 7 } 8 } 9 10 class MyClass<Ta> 11 { 12 public void ShowInfo(Ta a) 13 { 14 Type t = typeof(Ta); 15 Console.WriteLine(t.FullName + "非泛型方法"); 16 } 17 public void ShowInfo<Ta>(Ta a) 18 { 19 Type t = typeof(Ta); 20 Console.WriteLine(t.FullName + "泛型方法"); 21 } 22 public void ShowInfo<Tb>(Ta a, Tb b) 23 { 24 Type t = typeof(Tb); 25 Console.WriteLine(t.FullName); 26 } 27 }
泛型方法的类型推断:c#语法中包含大量"<"和">"符号,所以导致代码的可读性和可维护性降低了,所以为了改变这种情况,c#编译器支持在调用一个方法时进行类型推断。例如下面的代码:myclass.ShowInfo("myClass", myclass);会调用ShowInfo<string>(string a, string b);
- 可验证性和约束:编译器在编译泛型代码时,会对它进行分析,以确保代码适用于当前已有或将来可能定义的任何类型。请查看下面的方法:
1 public static Boolean MethodTakingAnyType<T>(T o) { 2 T temp = o; 3 Console.WriteLine(o.ToString()); 4 Boolean b = temp.Equals(o); 5 return b; 6 }
看这个方法里面有一个临时变量temp。方法里面执行两次变量赋值和几次方法调用,无论T是值类型还是引用类型还是接口类型或者是委托类型这个方法都能工作。这个方法适用于当前存在的所以类型,也适用于将来可能定义的任何类型。比如你再看一下下面这个方法:
1 public static T MethodTakingAnyType<T>(T o1,T o2) { 2 if (o1.CompareTo(o2) < 0) 3 { 4 return o1; 5 } 6 else { 7 return o2; 8 } 9 }
在编译时会报如下错误(error CS0117:"T"不包含"CompareTo"的定义),因为并不是所有的类型都提供了CompareTo方法。那么在什么情况下T应该是什么类型呢?幸好,编译器和CLR支持一个称为约束的机制,可利用它使泛型变得真正有用!
- 约束的语法:请看下面这个方法名字后面的where
1 public static Boolean MethodTakingAnyType<T>(T o) where T : struct 2 { 3 T temp = o; 4 Console.WriteLine(o.ToString()); 5 Boolean b = temp.Equals(o); 6 return b; 7 }
c#的where关键字告诉编译器,为T指定的任何类型都必须是值类型。所以当你为T指定其它类型时,编译器会报错,例如你指定为string类型时(MethodTakingAnyType<string>("");)它会报错误 1 类型“string”必须是不可以为 null 值的类型才能用作泛型类型或方法
- 主要约束:一个类型参数可以指定零个或一个主要约束(也就是第一个约束)。主要约束可以是一个引用类型,它标识了一个没有密封的类,不能指定以下特殊类型:System.Object;System.Array;System.Delegate;System.MulticastDelegate;System.ValueType;System.Enum;
- 次要约束:一个类型参数可以指定零个或多个次要约束。次要约束是一个接口类型,指定一个接口类型约束时,是告诉编译器类型实参必须实现这个接口。还有一种次要约束称为类型参数约束,有时也称为裸类型约束,看下面的代码
1 public static Boolean MethodTakingAnyType<T, TBase>(T o) where T : TBase 2 { 3 T temp = o; 4 Console.WriteLine(o.ToString()); 5 Boolean b = temp.Equals(o); 6 return b; 7 }
T类型参数由TBase类型单数约束,也就是说不管T为什么类型,都必须兼容于TBase指定的类型实参。
- 构造器约束:一个类型参数可以指定零个或一个构造器约束。指定构造器约束相当于告诉编译器一个指定的类型实参必须要实现了公共无参构造器。请看下面的的代码:
1 public static Boolean MethodTakingAnyType<T, TBase>(T o) where T : new() 2 { 3 T temp = o; 4 Console.WriteLine(o.ToString()); 5 Boolean b = temp.Equals(o); 6 return b; 7 }
- 其它可验证性的问题:1,泛型类型变量的转型。下面的类型编译时出错,因为T可能为任何类型,无法保证能转型成功!
1 public static void MethodTakingAnyType<T, TBase>(T o) 2 { 3 int x = (int)o; 4 string s = (string)o; 5 }
2,将一个泛型类型变量设置为默认值:o = default(T);这样的话,不管T为值类型还是引用类型都可以成功,如果T为引用类型时就设置为null,如果T为值类型时将默认值设为0;
3,将一个泛型类型变量与Null进行比较:使用==或者=!将一个泛型类型变量于null进行比较都是合法的。但是如果T为值类型时,o永远都不会为null查看下面的代码:1 public static void MethodTakingAnyType<T, TBase>(T o) 2 { 3 if (o == null) { 4 //do something 5 } 6 }
4,将两个泛型类型变量相互比较:如果T是值类型,下面的代码就是非法的。
1 public static void MethodTakingAnyType<T>(T o1,T o2) 2 { 3 if (o1 == o2) { 4 5 } 6 }
- 约束的语法:请看下面这个方法名字后面的where
4,泛型在使用过程的注意事项
-
- 泛型不支持协变性
- 缺乏操作符约束或者“数值”约束
- 缺乏泛型属性,索引器和其它成员类型