C#语言学习笔记20 —— 泛型(Generic),好用常用,却难以说明白
C# 泛型类型
现在的编程语言都提供了一种解决代码重用的方案:泛型或模板。
C# 的泛型也是一样,它提供一种观念,把类型作为参数,用来设计类和方法(还有结构体、接口、委托)。
只有到了使用类和方法的时候,才需要指定具体的类型参数。
这里的类称为泛型类(Generic class),方法称为泛型方法(Generic method)。
C# 从2.0开始就支持泛型,后面版本进行了增强。
以 .Net 里的 List<T> 为例,它是泛型类型(generic type),尖括号 <> 里的 T 是类型参数(type parameter)。
像 List<T> 这样是类的蓝图,像 List<int> 这样才是真正的类,是List<T>的实例。int 在这里作为类型参数(type argument),在编译期间进行替换。
泛型的优点
C# 泛型有以下几个优点:
- 类型安全:编译时类型检查(Type safety)
- 代码重用:一个泛型类/方法可以处理多种数据类型(Less code and code is more easily reused)
- 性能提升:避免装箱和拆箱操作(Better performance)
- 代码简洁:减少重复代码(例如:使用泛型委托可避免定义多个委托类)
从 List<T> 看泛型
假如要实现一个列表功能,用来分别存储 int、string、DateTime 等数据。
对于这个例子,可以直接使用 .Net 的 List<T> 泛型类。
使用 List<int>、List<string>、List<DateTime> 类型,演示代码片段如下:
如果不使用泛型,可以想到下面两个方案。
方案A,创建 3 个类 MyListInt、MyListInt、MyListString,元素类型分别为 int、string、DateTime。
方案B,使用 ArrayList 类,元素类型为 object。
不难看出,在方案A中,我们不得不维护几份几乎相同的代码,这是代码重用问题; 在方案B中,每次访问元素对象,都要进行类型转换,这是类型安全问题和性能损耗问题。
如果使用 List<T> 泛型类,代码重用、类型安全、性能损耗等问题都得到较好解决。
泛型参数约束(Constraints )
约束是告诉编译器,类型参数要满足哪些条件。类型参数是一个占位符,在某些情况下,不告诉有关它更多的信息,有些操作是无法进行的。
常见几种约束:
where T : struct | T 必须是值类型 |
where T : class | T 必须是引用类型,class、interface、delegate、array |
where T : class? | T 必须是引用类型,nullable 或者 non-nullable ,class、interface、delegate、array |
where T : new() | T 必须可以使用 new() 创建 |
where T : <base class name> | T 必须派生于base class name |
where T : <interface name> | T 必须是或实现指定的接口 |
where T : U | T 必须派生于类型参数 U 所指的类型 |
对于约束
- 有些约束是相互排斥的
- 有些约束是讲究出现次序的
创建自定义泛型
下面是一个简单例子,说明泛型的基本语法:
下面是一个稍微复杂的例子,它创建一个泛型链表(这里使用 AddHead,表现像堆栈),又派生一个支持排序功能的泛型链表。 链表类型参数使用了 where T : IComparable<T> 约束。
完整代码如下:
.Net 里的泛型
下面是一些常见的集合类(Collections),包括泛型版本(generic-based)和对应的非泛型版本(nongeneric)。
推荐使用泛型版本。
泛型版本 | 非泛型版本 | 功能 |
Dictionary<TKey,TValue> | Hashtable | 字典、哈希表 |
List<T> | ArrayList | 列表、动态数组 |
Queue<T> | Queue | 队列 |
Stack<T> | Stack | 堆栈 |
SortedList<TKey,TValue> | SortedList | 有序列表 |
SortedDictionary<TKey,TValue> | 没有非泛型版本 | 有序字典 |
LinkedList<T> | 没有非泛型版本 | 链表 |
一些常见的泛型接口:
- System.IComparable<T>
- System.IEquatable<T>
- IComparer<T>
- IEqualityComparer<T>
- ICollection<T>
- IList<T>
- IDictionary<TKey,TValue>
- IEnumerable<T>
一些常见的泛型委托:
- Action<T>
- Func<T, TResult>
- Predicate<T>
- Comparison<T>
- Converter<TInput,TOutput>
泛型中的协变(covariance)和逆变(contravariance)
协变(covariance)和逆变(contravariance)允许在泛型委托、泛型接口以及数组中进行更灵活的类型转换。
为了方便以下表达,使用几个词语:更具体(more specific,派生类),更一般(less specific,基类)。
- 协变 covariance
Derived -> Base,IEnumerable<Derived> -> IEnumerable<Base>,特殊给一般。
- 逆变 Contravariance
Base -> Derived,Action<Base> -> Action<Derived>,一般给特殊。
某些泛型接口具有协变(covariance)类型参数;例如:IEnumerable<T> 、IEnumerator<T> 、IQueryable<T> 和 IGrouping<TKey,TElement>。
某些泛型接口具有逆变(contravariance)类型参数;例如:IComparer<T>、IComparable<T> 和 IEqualityComparer<T>。
在泛型接口和委托定义中,用 in 和 out 来声明 contravariance 和 covariance。
在公共语言运行时(CLR)中,与变体(Variant 指 covariance 或 Contravariance)有关事项的简短摘要如下:
- Variant 类型参数仅限于泛型接口和泛型委托类型。
- 泛型接口或泛型委托类型可以同时具有协变和逆变类型参数。
- 变体仅适用于引用类型;如果为 Variant 类型参数指定值类型,则该类型参数对于生成的构造类型是不变的。
- 变体不适用于委托组合。只能在这两个委托的类型完全匹配的情况下对它们进行组合。
- 从 C# 9 开始,支持协变返回类型。重写方法可以声明比它重写的方法派生程度更高的返回类型,而重写的只读属性可以声明派生程度更高的类型。
专业术语难懂,尝试用平白话说一下(不知道是否正确?):
- 主要是关于泛型接口(Generic interfaces)和泛型委托(Generic delegates)的。
- 在保证类型安全下,放宽类型要求。
- 主要是类库设计者要关心的,为的是让接口和委托使用范围更广、更方便。
- 它讲述的是,一个接口是否能赋值给另一个接口类型的变量,一个委托是否能赋值给另一个委托类型的变量,这样的故事。
- 对于返回类型,设计者:期望更一般(less specific,基类)的类型,使用者:可以返回更具体(more specific,派生类)的类型。特殊到一般。
- 对于参数类型,设计者:期望更一般(less specific,基类)的类型,使用者:可以作为更具体(more specific,派生类)的类型来使用。一般到特殊。比如,接受一般 animal 的委托,可以赋值给接受特殊 dog 的委托。
- 例子:一个方法要返回 IEnumerable<object>, 给它一个 IEnumerable<string>,不是很合理吗?协变(covariance)。
- 例子:一个方法要接受 Action<string> acs,要它处理 string;现在丢给它一个Action<object> aco, 只会处理 object 的,aco 很为难吗?逆变(contravariance)。
下面是一个泛型的协变和逆变演示例子:
结束语
泛型,是现代编程语言的一个基本思想。泛型类型,在 .Net 的类库中,已经广泛使用。在 C# 日常编码过程中,已经离不开泛型。