C#泛型基础
1.基本概念
.NET2.0新增的最大的特性是泛型。
我们先来看下定义在System.Collections.Generic下的List<T>:
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable
List类后边的紧跟着一个<T>,T被称为类型参数(type parameter),是一个真实实参的占位符,表明该类未定义实际的数据类型。
实际使用的时候,需要指定T的具体类型,如:List<string>,此处的string被称为类型实参(type argument),List<T>下所有的T都会被替换成string类型。
2.泛型的优点:实现了编译时的类型安全和算法重用
在泛型出现以前,只能通过传递object类型的参数或者返回object类型来实现通用性的方法。
比如IComparable接口的CompareTo方法签名如下:
int CompareTo(object obj)
由于System.Object是所有对象的基类,存在任意类型到object的隐式转换,所以可以向CompareTo方法传递任意类型的参数。
以下是int的IComparable接口实现:
public int CompareTo(object value) { if (value == null) { return 1; } if (!(value is int)) { throw new ArgumentException(Environment.GetResourceString("Arg_MustBeInt32")); } int num = (int)value; if (this < num) { return -1; } if (this > num) { return 1; } return 0; }
通过分析代码存在以下的问题:
1.编译时可以传递任意参数,但是实际上只有传递一个int类型的参数才有实际意义。假如传递一个非int类型的参数,会抛出异常。
2.当传递一个int类型的实参时,需要进行装箱和拆箱操作,会影响性能。
再看int对泛型接口IComparable<T>的实现:
public int CompareTo(int value) { if (this < value) { return -1; } if (this > value) { return 1; } return 0; }
只能向方法传递一个int类型,实现了编译时的安全同时避免了拆箱和装箱的操作。
而其他类型通过实现各自的IComparable<T>接口,同样实现了算法的重用。
再来举一个例子。
场景:向列表添加若干数字,计算数字的和。
先来看使用ArrayList的实现:
ArrayList list = new ArrayList(); list.Add(1); list.Add(2); list.Add(3.0); int count = 0; for (int i = 0; i < list.Count; i++) { count += (int)list[i]; }
这段代码编译的时候能通过,运行时会报InvalidCastException异常,类型转换失败。
ArrayList的Add方法的签名为:Add(object value),所以每次使用Add方法向列表添加int对象时,先要进行装箱操作。
此处故意使用了for循环而不是foreach循环,就是为了清晰的体现其中的拆箱操作以及类型转换。
再来看使用List<T>的实现:
List<int> list = new List<int>(); list.Add(1); list.Add(2); //list.Add(3.0); int count = 0; for (int i = 0; i < list.Count; i++) { count += list[i]; }
注释掉的那行在编译时报错,方法参数不匹配。
我们使用int来替换T,表示list中的所有T都替换为int类型。
List<T>的Add方法签名为:void Add(T item),鼠标移动到Add方法上,可以明显的看到方法的参数属性类型以及被替换为int,同理list[i]也是int类型。
3.泛型约束
假如你编译以下代码:
T Foo<T>()
{
return new T();
}
编译的时候会提示错误:“变量类型“T”没有 new() 约束,因此无法创建该类型的实例 ”。
将代码修改如下可以通过编译:
T Foo<T>() where T : new() { return new T(); }
此处通过where对泛型添加了约束。以下为可以添加的约束类型(MSDN):
约束 |
说明 |
T:struct |
类型参数必须是值类型。Nullable can be specified.' data-guid="6eb132c88993822c7977723af20162ad">Nullable can be specified.'>Nullable can be specified.' data-guid="6eb132c88993822c7977723af20162ad">可以指定除 Nullable <T>以外的任何值类型。 |
T:class |
类型参数必须是引用类型;这一点也适用于任何类、接口、委托或数组类型。 |
T:new() |
类型参数必须具有无参数的公共构造函数。new() constraint must be specified last.' data-guid="012d33600c2a5348f794e49b607a570e">new() constraint must be specified last.'>new() constraint must be specified last.' data-guid="012d33600c2a5348f794e49b607a570e">当与其他约束一起使用时,new() 约束必须最后指定。 |
T:<基类> |
类型参数必须是指定的基类或派生自指定的基类(非密封类)。不能指定以下的类型:Object,Array,Delegate,MulticastDelegate,ValueType,Enum和Void |
T:<接口> |
类型参数必须是指定的接口或实现指定的接口。 可以指定多个接口约束。 约束接口也可以是泛型的。 |
T:U |
为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。 |
假如没有泛型约束功能,只能对泛型实例赋值或者使用object定义的方法。
通过使用泛型约束限制T的类型范围,才能在T的类型或者实例上调用相应的方法。
示例:
T Foo<T>(T t1,T t2) where T :IComparable<T> { if (t1.CompareTo(t2) > 0) { return t1; } return t2; }
很显然的只有对象实现了IComparable<T>接口,才能在对象的实例上调用CompareTo方法。
4.泛型方法
泛型类中的方法定义了自己的类型参数时,才能作为一个泛型方法。
如下代码:只有方法二才是一个泛型方法,方法一不过是使用了泛型类的类型参数作为形参或者返回结果而已。
public class Test<T> { public T GetInster() { return default(T); } public TValue GetFirstItem<TValue>(TValue[] list) { if (list == null) { throw new ArgumentNullException(); } if (!list.Any()) { throw new ArgumentException(); } return list[0]; } }
编译器在使用泛型方法时,能够通过变量的数据类型自动推断要使用的类型,使用上面的GetFirstItem作为示例:
int[] list = new int[] { 1, 2, 3 }; GetFirstItem<int>(list); GetFirstItem(list);
第三行的方法根据参数的类型,自动的推导出类型参数为int。
5.泛型的协变和逆变
泛型委托和泛型接口的类型参数可以标记为协变性量或者逆变量,
逆变量:泛型类型参数可以从一个基类变为该类的派生类,使用in关键字标记类型参数,逆变量只能出现在输入位置:方法的参数或者set访问器中的方法。
协变量:泛型类型参数可以从一个派生类变为该类的基类,使用out关键字标记类型参数,协变量只能出现在输出位置:方法的返回值。
对以下的泛型委托类型定义:
delegate TResout Func<in T, out TResout>(T t);
T被申明为逆变量,TResout被申明为协变量。
MyFunc<object, string> fn1 = s => s.ToString(); MyFunc<string, object> f2 = fn1;
所以在需要一个object对象参数时,可以传递一个string类型对象。
返回一个string类型的,可以被视为object类型
6.其他
6.1泛型反射
查看
typeof(List<int>).ToString()
可以发现泛型的类型名为“System.Collections.Generic.List`1[System.Int32]”。
前面是List类的类型名,紧跟着一个`和一个数字,数字表示类型的参数个数,再接着参数的类型。
6.2泛型类的构造函数
针对不同的类型参数,泛型类只在第一次调用的时候实时编译,所以第一次调用一个静态类会造成性能损失,以后再调用这个参数就能直接获取了。同时所有的引用类型实参都只会编译一次,因为所有的引用类型都是指向托管堆的指针。假如在某个泛型类中定义了静态字段或者静态构造器,针对不同类型的泛型实现,都拥有各种的静态字段,
6.3泛型实例的默认值
可以通过default(T)来设置泛型实例的默认值,类似逻辑为T是typeof(T).IsValueType ? 0 : null(此处的0是指将值类型的所有位设置为0)。