温故而知新-C#泛型
泛型类
除了有泛型方法,还有泛型类
1 | public class DocumentManager<T> |
默认值
可以看到在GetDocument
这个方法中初始化doc
变量时,我们使用了default
关键字
default关键字根据上下文可以有多种含义,switch语句中使用default定义默认情况。在泛型中取决于泛型类型是值类型还是引用类型,泛型default关键字将泛型类型初始化为null或0
泛型接口
泛型类都有了,泛型接口也可以有
1 | public interface IDocumentManager<T> |
还有泛型委托
1 | public delegate void Hello<T>(T t); |
注意:
1、泛型在声明的时候可以不指定具体的类型,在使用时需要指定具体类型
1 | public class AClass:BClass<int>{} |
2、如果子类也是泛型的,那么继承的时候可以不指定具体类型
1 | public class AClass<T>:BClass<T>{} |
逆变和协变
在.NET4.0
之前,泛型接口是不变的。
协变和逆变指对参数和返回值的类型进行转换。只能放在接口或委托的泛型参数前面
out
协变 covariant 用来修饰返回值;
in
逆变 contravariant 用来修饰传入参数;
示例:
定义一个Animal
类,再定义一个Cat
类继承Animal
1 | public class Animal |
调用:
1 | Animal animal = new Animal(); |
这个时候可以使用协变
1 | IEnumerable list = new List<Cat>(); |
可以看到,泛型接口使用了out
参数修饰,T
只能是返回值类型,不能作为参数类型。使用协变后,左边声明的是基类,右边可以声明子类或基类。
协变也可用于委托:
1 | Func<Animal>func = new Func<Cat>(()=>null); |
自定义协变
out协变,只能是返回结果
1 | public interface ICustomerListOut<out T> |
1 | ICustomerListOut<Animal> list1 = new CustomerListOut<Cat>(); |
自定义逆变
in逆变,只能作为方法参数,不能作为返回值。
1 | public interface ICustomerListIn<in T> |
1 | ICustomerListIn<Cat> list2 = new CustomerListIn<Animal>(); |
泛型约束
如果泛型类需要调用泛型类型中的方法,则需要添加约束。
定义一个IDocument
接口,定义两个只读属性
1 | public interface IDocument |
如果要显示Document的标题,我们可以这样写:
将T
强制转换为IDocument
接口
1 | public void DisplayAllDocuments() |
但是如果我们实现的不是IDocument
呢?而是其他的,并没有Title
,这个时候就会报错。
这个时候我们可以给DocumentManager
一个约束:T类型必须实现IDocument接口,使用Where
关键字
1 | DocumentManager<T>:IDocumentManager<T> where T : IDocument |
结果:
类型
约束 | 说明 |
---|---|
where T:struct | T类型必须是值类型 |
where T:class | T类型必须是引用类型(类、接口、委托、数组等) |
where T:IFoo | 指定类型必须实现接口IFoo |
where T:Foo | 指定类型T必须派生自基类Foo |
where T:new() | 构造函数约束,指定类型T必须有一个默认构造函数,需最后指定 |
where T1:T2 | T1派生自泛型类型T2 |
new()只能为默认构造函数定义构造函数约束,不能为其他构造函数定义构造函数约束
基类约束时,基类不能是密封类(即sealed类),sealed类不能被继承,则此约束无意义。
泛型约束可以多个约束:
1 | public void Show<T>(T t) where T:Document,new(){} |
泛型继承
1 | public class DocumentManager<T> : IDocumentManager<T> where T : IDocument |
泛型类型可以实现泛型接口,也可以派生自一个类。泛型类可以派生自泛型基类:
1 | public class Base<T>{} |
必须重复实现接口的泛型类型,或者必须执行基类的类型
1 | public class Base<T>{} |
派生类可以是泛型类或非泛型类
例子:定义一个抽象的泛型类,在派生类中用另一种实现
1 | public abstract class Calc<T> |
静态成员
泛型类的静态成员只能在类的一个实例中共享
1 | public class StaticDemo<T> |
结果:
泛型缓存
我们知道,类的静态构造函数只会执行一次,所以不管无论实例化多少次,在内存中只会有一个。
在泛型中,T
类型不同,每个不同的T
类型都会生成一个不同的副本,会产生不同的静态属性、静态构造函数。
1 | for (var i = 0; i < 5; i++) |
结果:
泛型会为不同类型都创建一个副本,构造函数执行5次,后面获取的缓存都是相同的。
注意:只能为不同的类型缓存一次。泛型缓存比字典缓存效率高,但是不能主动释放。
泛型结构
与类一样,结构也可以是泛型的。但是不同于类,不能继承。
以Nullable<T>
为例:
1 | public struct Nullable<T> where T: struct |
使用:
1 | Nullable<int> x; |
可空类型
在c#
中,使用?
定义可空类型
1 | int? i = 0; |
- 可空类型可以与
null
或数字比较 - 可空类型可以与算数运算符使用
非可空类型可以转换成可空类型,隐式转换
1 | int x = 4; |
可空类型转为非可空类型可能会失败,如果可空类型null
赋值给非可空类型则会抛出InvalidOperationException
异常,需要强制类型转换
1 | int? x = GetNullableType(); // 可能返回null |
如果不进行显示转换,则可以使用合并运算符从可空类型转换成非可空类型,关键词??
,为转换定义一个默认值,以防可控类型的值为null
。
1 | int? x = GetNullableType(); |