C#语言学习笔记20 —— 泛型(Generic),好用常用,却难以说明白

yumo6668小时前技术文章2

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# 日常编码过程中,已经离不开泛型。

相关文章

C语言的数据类型

C语言的数据类型在C语言中,数据类型用于定义变量存储的数据种类和大小,主要分为以下几类:1. 基本数据类型(Primary Data Types)(1) 整数类型类型存储大小(通常)取值范围说明cha...

仓颉编程语言编程技巧-模式匹配

什么是模式匹配仓颉编程语言中支持使用模式匹配表达式(match 表达式)实现模式匹配(pattern matching),允许开发者使用更精简的代码描述复杂的分支控制逻辑。直观上看,模式描述的是一种结...

C#语言学习笔记19 —— C# 程序的结构,已做开发多年,还未认真看过

C# 程序的结构C# 程序由一个或多个文件(.cs 文件)组成。每个文件包含 0 个或多个命名空间。命名空间包含类、结构、接口、枚举、委托等类型或其他命名空间。在写本笔记时(2025年5月),.Net...

溧阳计算机二级学习基础内容是哪些

以下是关于计算机二级学习的一些建议: ### 明确考试目标 计算机二级考试有多个科目可供选择,包括Java语言程序设计、Access数据库程序设计、MySQL数据库程序设计、Web程序设计、Pytho...

Python 面向对象:掌握类的继承与组合,让你的代码更高效!

引言:构建高效代码的基石Python以其简洁强大的特性,成为众多开发者首选的编程语言。而在Python的面向对象编程(OOP)范畴中,类的继承和组合无疑是两大核心概念。它们不仅能帮助我们实现代码复用,...