历史背景
泛型(Generic) 是C# 2 提出的新功能,并非语法糖。
在 C# 1 时代没有泛型。想要保存一组数据,并将其内容打印出来。可以使用:
数组
使用数组需要首先规划好数组的大小。如果超过数组大小显限制需要开辟新的空间并将数据复制过去,比较麻烦。ArrayList
解决了手动扩容问题,但为了通用性底层使用了object类型的数组。但又抛出了新的问题:- object类型装箱拆箱比较浪费性能。
- 如果类型不匹配,在操作数据的时候很可能出现异常。
CollectionBase
StringCollection是派生自 CollectionBase。虽然解决了性能问题,但对开发者来说,不同数据类型都要手动写一个对应的工具类。增加了开发和维护的工作量。
对此 C# 1 束手无策。
泛型的出现
泛型出现的目的是:实现使用同一个方法,满足不同参数做相同的事情。
优势是:相较object类型,无需装箱拆箱操作,无性能损失。
泛型的原理
泛型将参数类型的声明推迟到方法调用的时候。依赖.net framework 2.0 的编译器与JIT。
在代码编译期间将给泛型加个占位符。看下如下编译后的IL代码:
1 | .class public auto ansi beforefieldinit ClassLibrary1.CustomList`1<T> |
List<T>
转换成了 CustomList
1<T>`。其中:
- 数字’1’:值泛型的度为1。
- T:类型占位符。
只有在运行的时候根据T的不同类型填充进去在进行执行,这依赖JIT。
但是对于Unity il2cpp。没有jit怎么办?
答:编译器在编译过程中会收集所有 T 用到的类型(没用到的就回被剪裁掉)。然后生成对应的cpp方法。
泛型的应用
- 泛型类
- 泛型方法
- 泛型委托
- 泛型接口
泛型约束
基类约束。要求 T 必须是
Fruit
或者Fruit的派生类
。1
2
3
4public class Fruit {}
public class Banana : Fruit {}
class Test<T> where T : Fruit {}接口约束
1
2public interface IDisposeble{ void dispose(); }
class Test2<T> where T : IDisposeble{ }引用类型约束。
1
class Test3<T> where T : new { T defaultT = null; }
公有无参构造方法约束
如果有过个类型的约束时,new()
必须放在最后。1
class Test4<T> where T : new() { T defaultT = new T(); }
值类型约束
1
class Test5<T> where T : struct { T defaultT = default(T); }
泛型的协变/逆变
如下代码,Banana是派生自Fruit,Babana可以转换为Fruit, 但是将派生类的集合(List<Banana>)转换为基类的集合(List<Fruit>)是不允许的。
1 | public class Fruit { } |
为了结局这个问题,C# 4.0 提供的解决方案。
协变
如:string->object (子类到父类的转换),使用 out
关键字。
1 | public interface ICustomListOut<out T> { } |
逆变
如:object->string (父类到子类的转换),使用 in
关键字。
1 | public interface ICustomListIn<in T> { } |