1泛型是什么
泛型(Generic)就是写类或方法时先不把数据类型定死,用一个类型参数(习惯叫 T)占住位置,等到用的时候再在尖括号里写上具体类型。这样同一份代码就能和各种类型一起工作。
编译器只生成一份泛型定义,程序运行时用到 int、char,才做出各自的版本(后面用反射能看到)。视频里把泛型比成一台托盘机:卡座上插一张 T 空白卡,换成 int 卡,格子就只收 int。
2泛型类
类名后面加 <T>,里面的数组元素类型就写 T:
| 6 | public class MyGenericArray<T> | // 类名后加 <T>:T 是类型参数,先占住位置 |
| 7 | { | // MyGenericArray 类开始 |
| 8 | private T[] array; | // 数组的元素类型写 T,用的时候才定下来 |
| 9 | public MyGenericArray(int size) | // 构造函数,参数是数组大小 |
| 10 | { | // 构造函数开始 |
| 11 | array = new T[size + 1]; | // 开 size + 1 格:传 5 实际开 6 格,最后一格没用到 |
| 12 | } | // 构造函数结束 |
用的时候在尖括号里换上具体类型,同一个类先用 int,再换成 char(charArray. 放进 a 到 e):
| 28 | MyGenericArray<int> intArray = new MyGenericArray<int>(5); | // 写 <int>:这一刻 T 就换成了 int |
| 29 | // 设置值 | |
| 30 | for (int c = 0; c < 5; c++) | // c 从 0 到 4,循环 5 次 |
| 31 | { | // for 开始 |
| 32 | intArray.setItem(c, c*5); | // 第 c 格放 c × 5:0、5、10、15、20 |
| 41 | MyGenericArray<char> charArray = new MyGenericArray<char>(5); | // 同一个泛型类,换成 <char> |
查看完整代码(56 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | using System.Collections.Generic; | // 引入泛型集合所在的命名空间 |
| 3 | | |
| 4 | namespace GenericApplication | // 定义命名空间 GenericApplication |
| 5 | { | // 命名空间开始 |
| 6 | public class MyGenericArray<T> | // 泛型类 MyGenericArray,T 是类型参数 |
| 7 | { | // MyGenericArray 类开始 |
| 8 | private T[] array; | // 元素类型为 T 的数组 |
| 9 | public MyGenericArray(int size) | // 构造函数,参数是大小 |
| 10 | { | // 构造函数开始 |
| 11 | array = new T[size + 1]; | // 开 size + 1 格 |
| 12 | } | // 构造函数结束 |
| 13 | public T getItem(int index) | // getItem:按下标取出一个 T |
| 14 | { | // getItem 开始 |
| 15 | return array[index]; | // 返回第 index 格的值 |
| 16 | } | // getItem 结束 |
| 17 | public void setItem(int index, T value) | // setItem:把一个 T 放进第 index 格 |
| 18 | { | // setItem 开始 |
| 19 | array[index] = value; | // 存进数组 |
| 20 | } | // setItem 结束 |
| 21 | } | // MyGenericArray 类结束 |
| 22 | | |
| 23 | class Tester | // 定义 Tester 类 |
| 24 | { | // Tester 类开始 |
| 25 | static void Main(string[] args) | // 程序入口 Main 方法 |
| 26 | { | // Main 开始 |
| 27 | // 声明一个整型数组 | |
| 28 | MyGenericArray<int> intArray = new MyGenericArray<int>(5); | // T 换成 int,传 5 |
| 29 | // 设置值 | |
| 30 | for (int c = 0; c < 5; c++) | // c 从 0 到 4 |
| 31 | { | // 第 1 个 for 开始 |
| 32 | intArray.setItem(c, c*5); | // 第 c 格放 c × 5 |
| 33 | } | // 第 1 个 for 结束 |
| 34 | // 获取值 | |
| 35 | for (int c = 0; c < 5; c++) | // 再循环 5 次 |
| 36 | { | // 第 2 个 for 开始 |
| 37 | Console.Write(intArray.getItem(c) + " "); | // 取出第 c 格,后面跟一个空格,不换行 |
| 38 | } | // 第 2 个 for 结束 |
| 39 | Console.WriteLine(); | // 换行,第一行输出完 |
| 40 | // 声明一个字符数组 | |
| 41 | MyGenericArray<char> charArray = new MyGenericArray<char>(5); | // T 换成 char |
| 42 | // 设置值 | |
| 43 | for (int c = 0; c < 5; c++) | // 循环 5 次 |
| 44 | { | // 第 3 个 for 开始 |
| 45 | charArray.setItem(c, (char)(c+97)); | // 放进 c + 97 对应的字符 |
| 46 | } | // 第 3 个 for 结束 |
| 47 | // 获取值 | |
| 48 | for (int c = 0; c < 5; c++) | // 再循环 5 次 |
| 49 | { | // 第 4 个 for 开始 |
| 50 | Console.Write(charArray.getItem(c) + " "); | // 取出字符,后跟空格 |
| 51 | } | // 第 4 个 for 结束 |
| 52 | Console.WriteLine(); | // 换行,第二行输出完 |
| 53 | Console.ReadKey(); | // 等待按键(现在的 VS 不写也会停住) |
| 54 | } | // Main 结束 |
| 55 | } | // Tester 类结束 |
| 56 | } | // 命名空间结束 |
0 5 10 15 20 a b c d e
第二行是 a 到 e:( 把数字转成字符,97 正好是小写 a 的编码,加 1 就是 b。
3类型安全
往 MyGenericArray<int> 里放字符串 "abc",编译时就被拦下来:
| 40 | intArray.setItem(0, "abc"); | // intArray 的 T 是 int,却传进 string |
Program.cs(40,33): error CS1503: 参数 2: 无法从“string”转换为“int”
4泛型的特点
- 重用代码:一份
MyGenericArray,int、char都能用。 - 类型安全:类型不对,编译时就报错,就是上面的例子。
- 提高性能:上一章的
ArrayList什么都当object存,存int要先装箱(把值类型包成 object),取出来还要强转;泛型集合直接存int,这两步都省了。 - 现成的泛型集合:在
System.命名空间里,Collections. Generic ArrayList换成List,Hashtable换成Dictionary,Stack、Queue、SortedList也都有泛型版。它们 2005 年的 .NET Framework 2.0 就有了(教程说的“新”已过时),现在是写新代码的首选。 - 自己写泛型:可以写泛型接口、泛型类、泛型方法和泛型委托,还能用约束限制
T能换成哪些类型(后面讲)。
事件本身不能带类型参数(教程列的“泛型事件”这样写会报错):
| 5 | public event EventHandler<T>? Changed<T>; | // 想给事件 Changed 加 <T>,这样写不行 |
Program.cs(5,35): error CS7002: 意外使用了通用名称
Program.cs(5,45): error CS1514: 应为 {
Program.cs(5,45): error CS1055: 应为 add 访问器或 remove 访问器
Program.cs(13,2): error CS1513: 应输入 }正确做法是用泛型委托类型来声明事件,比如 EventHandler<int>,触发时处理方法收到的就是一个 int。Invoke 就是调用委托,this 指当前这个 Thermo 对象:
| 5 | public event EventHandler<int>? Changed; | // 事件类型是 EventHandler<int>;? 表示可以为空(还没人订阅) |
| 6 | | |
| 7 | public void Set(int v) | // Set 方法:传进一个 int |
| 8 | { | // Set 开始 |
| 9 | Changed?.Invoke(this, v); | // ?. 表示有人订阅才触发,把自己和 v 交出去 |
| 15 | static void Show(object? sender, int v) | // sender 是触发事件的对象;v 直接是 int |
| 16 | { | // Show 开始 |
| 17 | Console.WriteLine("changed to " + v); | // 输出收到的数 |
changed to 30
5运行时查泛型类型
泛型用的是什么类型,运行时能用反射(程序运行时查看类型信息的功能)查到:
| 8 | List<int> a = new List<int>(); | // 装 int 的 List |
| 9 | List<char> b = new List<char>(); | // 装 char 的 List |
| 10 | Console.WriteLine(a.GetType()); | // GetType() 取到对象的实际类型 |
| 11 | Console.WriteLine(b.GetType()); | // 再看 b 的类型 |
| 12 | Console.WriteLine(a.GetType().GetGenericArguments()[0]); | // GetGenericArguments() 取出类型参数,[0] 是第一个 |
System.Collections.Generic.List`1[System.Int32] System.Collections.Generic.List`1[System.Char] System.Int32
打出来是两个不同的类型:反引号后面的 1 表示有一个类型参数,方括号里一个是 Int32,一个是 Char。用到哪种类型,运行时就有了哪个版本。
6泛型方法
泛型也能用在方法上。方法名后加 <T>,两个参数都是 T;ref 表示直接改调用者传进来的变量:
| 8 | static void Swap<T>(ref T lhs, ref T rhs) | // 泛型方法 Swap:交换两个 T 类型的变量 |
| 9 | { | // Swap 开始 |
| 10 | T temp; | // 借一个临时变量 temp |
| 11 | temp = lhs; | // 第 1 步:lhs 的值存进 temp |
| 12 | lhs = rhs; | // 第 2 步:rhs 的值放进 lhs,lhs 原来的值被覆盖 |
| 13 | rhs = temp; | // 第 3 步:temp 里存着的旧值放进 rhs |
| 14 | } | // Swap 结束 |
| 31 | Swap<int>(ref a, ref b); | // int 版:a = 10、b = 20 交换后 a = 20、b = 10 |
| 32 | Swap<char>(ref c, ref d); | // char 版:c = 'I'、d = 'V' 交换后 c = 'V'、d = 'I' |
查看完整代码(42 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | using System.Collections.Generic; | // 引入泛型集合所在的命名空间 |
| 3 | | |
| 4 | namespace GenericMethodAppl | // 定义命名空间 GenericMethodAppl |
| 5 | { | // 命名空间开始 |
| 6 | class Program | // 定义 Program 类 |
| 7 | { | // Program 类开始 |
| 8 | static void Swap<T>(ref T lhs, ref T rhs) | // 泛型方法 Swap,参数用 ref 传 |
| 9 | { | // Swap 开始 |
| 10 | T temp; | // 临时变量 temp |
| 11 | temp = lhs; | // lhs 存进 temp |
| 12 | lhs = rhs; | // rhs 放进 lhs |
| 13 | rhs = temp; | // temp 放进 rhs |
| 14 | } | // Swap 结束 |
| 15 | static void Main(string[] args) | // 程序入口 Main 方法 |
| 16 | { | // Main 开始 |
| 17 | int a, b; | // 声明两个 int |
| 18 | char c, d; | // 声明两个 char |
| 19 | a = 10; | // a 是 10 |
| 20 | b = 20; | // b 是 20 |
| 21 | c = 'I'; | // c 是大写 I |
| 22 | d = 'V'; | // d 是大写 V |
| 23 | | |
| 24 | // 在交换之前显示值 | |
| 25 | Console.WriteLine("Int values before calling swap:"); | // 提示:交换前的 int |
| 26 | Console.WriteLine("a = {0}, b = {1}", a, b); | // 输出 a = 10, b = 20 |
| 27 | Console.WriteLine("Char values before calling swap:"); | // 提示:交换前的 char |
| 28 | Console.WriteLine("c = {0}, d = {1}", c, d); | // 输出 c = I, d = V |
| 29 | | |
| 30 | // 调用 swap | |
| 31 | Swap<int>(ref a, ref b); | // T 换成 int,交换 a、b |
| 32 | Swap<char>(ref c, ref d); | // T 换成 char,交换 c、d |
| 33 | | |
| 34 | // 在交换之后显示值 | |
| 35 | Console.WriteLine("Int values after calling swap:"); | // 提示:交换后的 int |
| 36 | Console.WriteLine("a = {0}, b = {1}", a, b); | // 输出 a = 20, b = 10 |
| 37 | Console.WriteLine("Char values after calling swap:"); | // 提示:交换后的 char |
| 38 | Console.WriteLine("c = {0}, d = {1}", c, d); | // 输出 c = V, d = I |
| 39 | Console.ReadKey(); | // 等待按键(现在的 VS 不写也会停住) |
| 40 | } | // Main 结束 |
| 41 | } | // Program 类结束 |
| 42 | } | // 命名空间结束 |
查看完整输出(8 行)
Int values before calling swap: a = 10, b = 20 Char values before calling swap: c = I, d = V Int values after calling swap: a = 20, b = 10 Char values after calling swap: c = V, d = I
Swap 后面的尖括号可以省掉,编译器看参数是 int,就知道 T 是 int(叫类型推断),运行结果一模一样:
| 31 | Swap(ref a, ref b); | // 不写 <int>,编译器从 a、b 推出 T 是 int |
| 32 | Swap(ref c, ref d); | // 不写 <char>,推出 T 是 char |
7约束 where
写一个 Max 方法返回两个值里大的那个,直接写 a > b 会报错,因为 T 可能是任何类型,编译器不知道它能不能比大小:
| 5 | static T Max<T>(T a, T b) | // 泛型方法 Max,没有约束 |
| 6 | { | // Max 开始 |
| 7 | return a > b ? a : b; | // ? : 是条件运算符:a 大就返回 a,否则 b |
Program.cs(7,16): error CS0019: 运算符“>”无法应用于“T”和“T”类型的操作数
加上 where T : IComparable<T>,意思是 T 必须能比较(IComparable<T> 本身就是泛型接口),就能调用 CompareTo 比大小:
| 5 | static T Max<T>(T a, T b) where T : IComparable<T> | // where 加约束:T 必须实现 IComparable<T> |
| 6 | { | // Max 开始 |
| 7 | return a.CompareTo(b) > 0 ? a : b; | // CompareTo 大于 0 表示 a 比 b 大 |
| 8 | } | // Max 结束 |
| 9 | | |
| 10 | static void Main() | // 程序入口 Main 方法 |
| 11 | { | // Main 开始 |
| 12 | Console.WriteLine(Max(3, 7)); | // 3 和 7 比,返回 7 |
| 13 | Console.WriteLine(Max("apple", "banana")); | // 按字母顺序 banana 排在后面,返回 banana |
7 banana
传进两个 object 就报错,object 没有实现这个接口:
| 14 | Console.WriteLine(Max(new object(), new object())); | // object 不满足 IComparable<T> 约束 |
Program.cs(14,27): error CS0311: 类型“object”不能用作泛型类型或方法“Program.Max<T>(T, T)”中的类型参数“T”。没有从“object”到“System.IComparable<object>”的隐式引用转换。
常用的约束
where T : struct:T必须是值类型,int可以,string报错。where T : class:T必须是引用类型,正好反过来。where T : new():T必须有无参数的构造函数,int可以,string报错。- 接口约束(如上面的
IComparable<T>):int和string都满足;还可以写某个基类的名字,要求T派生自它。
| 5 | static void NeedStruct<T>() where T : struct { } | // 要求 T 是值类型;{ } 是空方法体 |
| 6 | static void NeedClass<T>() where T : class { } | // 要求 T 是引用类型 |
| 7 | static void NeedNew<T>() where T : new() { } | // 要求 T 有无参数构造函数 |
| 8 | | |
| 9 | static void Main() | // 程序入口 Main 方法 |
| 10 | { | // Main 开始 |
| 11 | NeedStruct<int>(); | // int 是值类型,通过 |
| 12 | NeedClass<string>(); | // string 是引用类型,通过 |
| 13 | NeedNew<int>(); | // int 有无参数构造,通过;最后输出 OK |
换成反过来的类型,三行都报错:
| 11 | NeedStruct<string>(); | // string 不是值类型 |
| 12 | NeedClass<int>(); | // int 不是引用类型 |
| 13 | NeedNew<string>(); | // string 没有无参数构造函数 |
Program.cs(11,9): error CS0453: 类型“string”必须是不可为 null 值的类型,才能用作泛型类型或方法“Program.NeedStruct<T>()”中的参数“T” Program.cs(12,9): error CS0452: 类型“int”必须是引用类型才能用作泛型类型或方法“Program.NeedClass<T>()”中的参数“T” Program.cs(13,9): error CS0310: “string”必须是具有公共的无参数构造函数的非抽象类型,才能用作泛型类型或方法“Program.NeedNew<T>()”中的参数“T”
8泛型委托
委托也能带类型参数,参数是 T,返回的也是 T。例子把它换成 int 来用:静态变量 num 初值 10,AddNum 给它加参数,MultNum 给它乘参数,getNum 读出它。
| 4 | delegate T NumberChanger<T>(T n); | // 泛型委托:接收一个 T,返回一个 T |
| 29 | NumberChanger<int> nc1 = new NumberChanger<int>(AddNum); | // T 换成 int,nc1 指向 AddNum |
| 30 | NumberChanger<int> nc2 = new NumberChanger<int>(MultNum); | // nc2 指向 MultNum |
| 31 | // 使用委托对象调用方法 | |
| 32 | nc1(25); | // 调用 AddNum(25):num = 10 + 25 = 35 |
| 33 | Console.WriteLine("Value of Num: {0}", getNum()); | // 输出 35 |
| 34 | nc2(5); | // 调用 MultNum(5):num = 35 × 5 = 175 |
| 35 | Console.WriteLine("Value of Num: {0}", getNum()); | // 输出 175 |
Value of Num: 35 Value of Num: 175
C# 还自带了现成的泛型委托,比如 Func 和 Action。
9小结
- 泛型用类型参数
T占位,用的时候在尖括号里写上具体类型。 - 泛型类、泛型方法、泛型委托都是这样写;泛型方法的尖括号常常可以省。
- 类型不对,编译时就报错,这就是类型安全。
- 要限制
T能换成哪些类型,就用where加约束。
下一节:让委托指向一段代码都得先写一个有名字的方法,能不能把代码直接写在用它的地方?
