C#泛型
泛型概述
泛型是CLR提供的一种特殊机制,让我们可以先用“类型占位符”来写代码,然后在创建类型的实例时提供真正的类型。在很多时候我们可以把类的行为提取或者重构出来,增加一个抽象层,使之不仅能用于硬编码的类型上,而且可以应用到其他的类型上,即另外一种形式的代码重用。泛型的优点如下:
- 不管构造类型的数量有多少,只有一个实现,有效的减少了代码膨胀,同时代码的可读性更好更加易于维护。
- 泛型提供了一个强类型的编程模型,它能确保只有成员希望的数据类型才可以使用。e.g new List<DateTime>().Add("string"); 在编译时会报错 The best overloaded method match for 'System.Collections.Generic.List<System.DateTime>.Add(System.DateTime)' has some invalid arguments ;Argument 1: cannot convert from 'string' to 'System.DateTime' ,因为string不能转换成DateTime。
- 编译时的类型检查可以有效的减小在CLR发生InvalidCastException异常几率。
- 当泛型类成员使用值类型时,不再造成到object的强制转换,他们不再需要装箱或者拆箱操作,减小了内存的消耗,性能得到了提高。
泛型类
泛型类型仍然是类型,在使用非泛型类的过程中有两个步骤:声明类,创建类的实例。但是泛型类型不是实际的类型,而是类的模板,所以我们必须先构建实际的类型,然后创建这个实际类型的实例,实际上是在CLR中定义一个新的类型对象,新的类型对象是从泛型类型派生自哪个类型对象。(e.g:MyGeneric<T>派生自Object 所以MyGeneric<DateTime>, MyGeneric<String>也派生自Object)。
泛型类参数命名指导规则:参数命名应该尽量具有描述性,名称还应该以T为前缀,eg: TKey,TValue,TEntity多个类型的参数:泛型类型可以使用任意数量的类型参数 e.g:Action<T,T1,...Tn>
参数约束:约束使用where子句 where TypeParam:constraint,constraint....如果有构造器约束,必须放在最后。
- 类型名称:只有这个类型的类或者从它继承的类才能作参数 where T:classname
- 接口约束:只有这个接口或者实现这个接口的类型才能作参数,可以有多个接口约束 were T:interfacename,interfacename
- class约束:任何引用类型,包括类、数组、委托、接口等都可以作参数 where T:class
- struct约束:任何值类型都可以做参数 where T:struct
- 构造函数约束:任何带有无参公共构造函数的类型都可以用作参数 where T:new()
泛型方法:泛型方法可以在泛型类非泛型类结构和接口中声明,泛型方法和泛型类一样有类型参数列表和可选的约束列表。
public class MyClass { internal static void Print<T>(T[] array) { foreach (var item in array) { Console.Write(item.ToString() + " "); } Console.WriteLine(); } } public class Program { private static void Main(string[] args) { var intArray = new int[] {1,2,3,4,5}; var stringArray = new string[] { "hello", "world" }; var doubleArray = new double[] {1.12,2.25,3.141592654}; MyClass.Print(intArray); MyClass.Print<int>(intArray); MyClass.Print(stringArray); MyClass.Print<string>(stringArray); MyClass.Print(doubleArray); MyClass.Print<double>(doubleArray); Console.ReadKey(); } }
泛型方法重载:C#是依据方法名称,参数类型,参数数量,参数顺序来定义函数的唯一性,并不包括方法的返回值类型,因此我们可以从这几个方面来看泛型方法的重载。
不能进行重载的方法组有:
- public void MyMethod<T>(T t) {}
- public void MyMethod<U>(U u) {}
- public void MyMethod<T>(T t) where T : BaseA{}
- public void MyMethod<U>(U u) where U : BaseB{}
- public T MyMethod<T>(T t) {}
- public U MyMethod<U>(U u) {}
可以重载的方法组有:
- public void MyMethod<T>(T t) { }
- public void MyMethod(int i) {}
- public void MyMethod(T t, U u) {}
- public void MyMethod(U u, T t) { }
- public void MyMethod(U u, T t,int a) { }
泛型方法的重写:重写一个虚泛型方法是,或者创建一个显式接口方法实现时,约束是隐式继承的,不可以重新声明。
public class BaseClass { public virtual void Mymethod<T>(T t) where T : new() { Console.WriteLine("base"); } } public class SubClass : BaseClass { public override void Mymethod<X>(X x) // where X:IComparable 在重写的过程中,抽象类中的抽象方法的约束是被默认继承的 因此不能直接指定约束 { Console.WriteLine("sub"); base.Mymethod<X>(x); } }
泛型接口
泛型接口允许我们编写参数和接口成员返回类型是泛型类型参数的接口。泛型接口的声明和非泛型类型接口声明基本一致,只是声明接口名称后添加类型参数。实现泛型类型接口时,必须没有可能的类型实参组合会在类型中产生两个重复的接口。下面的例子中,如果第二个接口把int作为类型实参,类型就会有两个相同的接口。
interface IGeneric<T> { T ReturnValue(T value); } class MyGeneric<U>:IGeneric<int>,IGeneric<U> { public int ReturnValue(int value) { throw new NotImplementedException(); } public U ReturnValue(U value) { throw new NotImplementedException(); } }
- 泛型接口同样提供了出色的编译时类型安全性
- 处理值类型时,装箱次数会少很多
- 同一个类可以实现同一个接口若干次,只要每次使用不同的类型参数
逆变和协变
每个泛型类型的参数都可以标记为协变量或者逆变量。利用这个功能,可以将泛型类型的一个变量转换成另外一个变量,从而大大提高了泛型参数的兼容性。
- 不变量:意味着泛型类型参数不能更改。
- 逆变量:意味着泛型类型参数可以从一个基类更改为该类型的派生类。用in关键字表示。逆变量的泛型类型参数只能出现在输入位置,比如做方法的参数。
- 协变量:意味着泛型类型参数可以从一个派生类更改为基类,用out关键字表示。协变量的泛型类型参数只能出现在输出位置,比如做方法的返回类型。