Skip to Content
Week 02Day 4 - 泛型

Day 4 - 泛型

建议用时:180-210 分钟

你将学会什么

  • 泛型是什么,为什么 List<int>List<string> 都叫 List
  • T 是什么,为什么它只是类型占位符
  • 泛型和 object 的区别
  • 泛型方法怎么写
  • 泛型类怎么写
  • 泛型约束 where 的基本用法
  • 如何用泛型写一个可复用的盒子、打印方法和结果类型

泛型不是高级魔法。先把它理解成“把类型也当成参数传进去”,这样同一套代码就能安全处理不同类型。

本页固定顺序

  1. 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
  2. 再学第二部分:把刚学的知识组合成一个完整例子。
  3. 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
  4. 最后做第四部分:先独立完成作业,再用完整答案检查。

学习衔接

上一页学习的是“LINQ 分组与聚合”,今天继续学习“泛型”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。

今天的最低通过线

第一次学习不要求背完整页。完成下面 3 项,就可以继续:

  • 能用自己的话说明“泛型”解决什么问题。
  • 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
  • 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。

第一部分:先学原理和最小知识

这一部分先解决“这是什么、为什么要这样写”。每看到一个短例子就亲手敲一遍并运行,不要先背完整程序。

今天先抓住什么

泛型解决的是这个问题:

逻辑一样,只是处理的数据类型不同

例如 List<int>List<string>

var numbers = new List<int> { 1, 2, 3 }; var names = new List<string> { "Tom", "Jerry" }; Console.WriteLine(numbers.Count); Console.WriteLine(names.Count);

它们都是 List,都有 AddCountforeach,只是里面装的数据类型不同。

名词先讲清楚

名词直接解释例子
泛型让类型变成可传入参数的写法List<T>
类型参数类型的占位符T
具体类型真正传进去的类型intstring
泛型类带类型参数的类Box<T>
泛型方法带类型参数的方法PrintItems<T>
类型安全编译时阻止放错类型List<int> 不能放字符串
约束限制 T 必须满足条件where T : class

T 到底是什么

T 不是一个真实类型,它只是占位符。

var intBox = new Box<int>(10); var textBox = new Box<string>("Tom"); Console.WriteLine(intBox.Value); Console.WriteLine(textBox.Value); class Box<T> { public T Value { get; } public Box(T value) { Value = value; } }

当你写:

new Box<int>(10) 里,T 可以理解成 int

可以理解成 T 被替换成了 int

当你写:

new Box<string>("Tom") 里,T 可以理解成 string

可以理解成 T 被替换成了 string

为什么不能全用 object

object 看起来也能保存任何类型:

var values = new List<object>(); values.Add(10); values.Add("Tom"); values.Add(19.9m); foreach (object value in values) { Console.WriteLine(value); }

但问题是:取出来时,C# 只知道它是 object,不知道它原来具体是什么。

下面这种代码就很危险:

object value = "Tom"; if (value is int number) { Console.WriteLine(number + 1); } else { Console.WriteLine("不是整数"); }

泛型更安全:

var numbers = new List<int>(); numbers.Add(10); // numbers.Add("Tom"); // 编译阶段就会拦住 Console.WriteLine(numbers[0] + 1);

错误越早暴露,越容易修。

List<T> 就是最常见的泛型

你前面已经一直在用泛型:

List<int> scores = new List<int>(); List<string> names = new List<string>(); Dictionary<string, decimal> prices = new Dictionary<string, decimal>();

尖括号里的内容就是类型参数:

写法意思
List<int>这个列表只能放整数
List<string>这个列表只能放字符串
Dictionary<string, decimal>key 是字符串,value 是金额

泛型方法

如果一个方法对不同类型做同样的事,可以写成泛型方法。

PrintValue(123); PrintValue("hello"); PrintValue(19.9m); void PrintValue<T>(T value) { Console.WriteLine(value); }

输出:

123 hello 19.9

这里 PrintValue<T> 不关心传进来的具体类型,只负责打印。

泛型类

泛型类适合保存或处理某一种类型的数据。

var result = new Result<string>(true, "保存成功"); Console.WriteLine(result.Success); Console.WriteLine(result.Data); class Result<T> { public bool Success { get; } public T Data { get; } public Result(bool success, T data) { Success = success; Data = data; } }

Result<string> 表示成功时携带一段字符串数据。也可以写成 Result<int>Result<Product>

泛型约束

有时候方法需要调用 T 身上的某种能力。没有约束时,C# 不知道 T 一定有什么方法或属性。

最基础的约束先认识两个:

写法意思
where T : classT 必须是引用类型
where T : new()T 必须能用无参数构造函数创建

例子:

Product product = Create<Product>(); Console.WriteLine(product.Name); T Create<T>() where T : new() { return new T(); } class Product { public string Name { get; set; } = "Default Product"; }

如果没有 where T : new()new T() 就不能写。

什么时候该写泛型

适合写泛型:

逻辑完全一样,只是类型不同

不适合写泛型:

每种类型处理规则完全不同

先写具体类型版本,发现只有类型不同、逻辑相同,再改成泛型。

泛型常用写法速查

写法直接解释常见场景
T一个类型占位符Box<T>List<T>
TItem表示列表里的单个元素类型PrintItems<TItem>
TResult表示返回结果类型Result<TResult>
TKey字典 key 的类型Dictionary<TKey, TValue>
TValue字典 value 的类型Dictionary<TKey, TValue>
where T : classT 必须是引用类型只接受类对象
where T : structT 必须是值类型只接受数字、bool、DateTime 等
where T : new()T 必须能 new T()通用创建对象

模板 1:泛型方法

void PrintValue<T>(T value) { Console.WriteLine(value); }

适合“对任何类型都做同一件事”。

模板 2:泛型列表方法

void PrintItems<TItem>(List<TItem> items) { foreach (TItem item in items) { Console.WriteLine(item); } }

TItem 比单独写 T 更容易读,意思是“列表里的元素类型”。

模板 3:泛型结果类型

class Result<TResult> { public bool Success { get; } public TResult? Data { get; } public string Error { get; } public Result(bool success, TResult? data, string error) { Success = success; Data = data; Error = error; } }

以后服务层经常需要返回:

成功还是失败 成功时的数据 失败时的错误原因

这时 Result<T> 很常用。

模板 4:字典里的两个泛型参数

var prices = new Dictionary<string, decimal>();

这里可以读成:

TKey = string TValue = decimal

也就是:用字符串 key 查金额 value。

今天写代码时按这个顺序

  1. 先看有没有重复逻辑。
  2. 判断重复点是不是只有类型不同。
  3. 把具体类型换成 T
  4. 调用时传入具体类型,例如 Box<int>
  5. 如果需要 new T() 或特定能力,再考虑约束。

第二部分:把知识组合成完整例子

前面已经学过最小知识。现在把它们组合起来,先读懂执行顺序,再完整敲一遍。今天最终要写一个通用盒子 Box<T>,它既能保存文字,也能保存数字。

最终你要写出这个程序

var nameBox = new Box<string>("Keyboard"); var countBox = new Box<int>(3); var priceBox = new Box<decimal>(199m); Console.WriteLine(nameBox.Value.ToUpper()); Console.WriteLine(countBox.Value + 1); Console.WriteLine($"{priceBox.Value:F2}"); class Box<T> { public T Value { get; } public Box(T value) { Value = value; } }

输出:

KEYBOARD 4 199.00

同一个 Box<T> 模板,创建 Box<string> 时保存字符串,创建 Box<int> 时保存整数,创建 Box<decimal> 时保存金额。

第三部分:跟着敲代码

从这里开始动手。每个例子都可以直接放进 Program.cs 运行。

例子 1:List<T> 的类型安全

var numbers = new List<int>(); numbers.Add(10); numbers.Add(20); foreach (int number in numbers) { Console.WriteLine(number + 1); }

预期输出:

11 21

如果尝试添加 "Tom",编译阶段就会报错。

例子 2:泛型方法打印任何类型

PrintValue(123); PrintValue("hello"); PrintValue(19.9m); void PrintValue<T>(T value) { Console.WriteLine(value); }

同一个方法可以接收不同类型。

例子 3:泛型方法打印列表

var names = new List<string> { "Keyboard", "Mouse" }; var scores = new List<int> { 80, 95 }; PrintItems(names); PrintItems(scores); void PrintItems<T>(List<T> items) { foreach (T item in items) { Console.WriteLine(item); } }

PrintItems 不关心列表里是字符串还是整数。

例子 4:泛型盒子 Box<T>

var nameBox = new Box<string>("Keyboard"); var countBox = new Box<int>(3); Console.WriteLine(nameBox.Value.ToUpper()); Console.WriteLine(countBox.Value + 1); class Box<T> { public T Value { get; } public Box(T value) { Value = value; } }

输出:

KEYBOARD 4

例子 5:通用结果类型 Result<T>

var success = new Result<decimal>(true, 199m, ""); var failure = new Result<decimal>(false, 0m, "价格格式不正确"); PrintResult(success); PrintResult(failure); void PrintResult<T>(Result<T> result) { if (result.Success) { Console.WriteLine($"成功: {result.Data}"); } else { Console.WriteLine($"失败: {result.Error}"); } } class Result<T> { public bool Success { get; } public T Data { get; } public string Error { get; } public Result(bool success, T data, string error) { Success = success; Data = data; Error = error; } }

这种结构以后在服务层、接口返回结果里很常见。

例子 6:where T : new() 约束

Product product = Create<Product>(); Console.WriteLine(product.Name); T Create<T>() where T : new() { return new T(); } class Product { public string Name { get; set; } = "Default Product"; }

这个例子说明:因为有 where T : new(),方法里才能写 new T()

常见错误和修法

错误为什么错修法
泛型参数名随便写读代码看不出含义常用 TTItemTResultTKey
泛型方法里调用不存在的属性T 不保证有这个属性加约束或传入选择函数
什么都写成泛型代码会变难懂多种类型逻辑相同时再用泛型
约束写得太宽方法内部仍然不能安全调用根据需要使用 where T : class 等约束
返回类型不明确调用方不知道拿到什么方法名和返回类型一起表达意图

小白重复敲写训练

泛型的符号容易忘,必须亲手多写几遍 <T>

训练 1:返回原值的泛型方法

Console.WriteLine(Echo(10)); Console.WriteLine(Echo("hello")); T Echo<T>(T value) { return value; }

改动任务:再调用一次 Echo(19.9m),观察同一个方法处理三种类型。

训练 2:打印两个同类型的值

PrintPair("Keyboard", "Mouse"); PrintPair(10, 20); void PrintPair<T>(T first, T second) { Console.WriteLine($"{first} | {second}"); }

改动任务:尝试 PrintPair(10, "Mouse"),阅读编译错误,再恢复代码。

训练 3:自己写一个泛型交换

string left = "A"; string right = "B"; Swap(ref left, ref right); Console.WriteLine($"{left}, {right}"); void Swap<T>(ref T first, ref T second) { T temp = first; first = second; second = temp; }

第二遍把 string 换成 int 再敲一次。

每日小测

做完本页后,用这 5 题检查是否真的掌握。

1. 判断题

本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。

答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。

2. 填空题

本页主题是:泛型。今天至少要掌握的 3 个点是:

1. 泛型是什么,为什么 `List<int>` 和 `List<string>` 都叫 `List` 2. `T` 是什么,为什么它只是类型占位符 3. 泛型和 `object` 的区别

答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。

3. 流程题

遇到本页相关功能时,先按什么顺序处理?

答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。

4. 找错误题

如果本页代码运行失败,第一步应该做什么?

答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。

5. 改需求题

在本页完整例子跑通后,至少改一个小需求。

可选改法:

  • 改一个字段名称。
  • 多加一个校验条件。
  • 多输出一行结果。
  • 把固定数据改成用户输入。
  • 把一次处理改成多条数据处理。

答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:泛型是什么,为什么 List<int> 和 List<string> 都叫 List。

上位机专项练习

泛型让同一个结果类型、缓冲区或处理方法同时支持温度、压力、状态等不同数据。

下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。

专项例子 1:泛型结果保存不同数据

Result<double> temperature = new(true, 25.6, ""); Result<string> status = new(true, "在线", ""); Console.WriteLine(temperature.Value); Console.WriteLine(status.Value); record Result<T>(bool Success, T Value, string Error);

运行结果或界面效果:

25.6 在线

改动任务: 增加一个 Result<int> 保存报警数量。

专项例子 2:泛型方法打印采集值

void PrintValue<T>(string name, T value) { Console.WriteLine($"{name}: {value}"); } PrintValue("温度", 25.6); PrintValue("在线", true);

运行结果或界面效果:

温度: 25.6 在线: True

改动任务: 传入字符串“运行中”。

专项例子 3:泛型 List 保存统一类型

var temperatures = new List<double> { 20.1, 20.5 }; var deviceNames = new List<string> { "PLC-01", "PLC-02" }; Console.WriteLine(temperatures.Count); Console.WriteLine(deviceNames.Count);

运行结果或界面效果:

2 2

改动任务: 尝试向 temperatures 添加字符串,观察编译错误。

第四部分:作业完整答案

这一部分给出当天作业的完整答案。先按第三部分做一遍,再用这里检查。

作业要求

写一个泛型练习程序,要求:

  1. 写一个 Box<T>,能保存任意类型的一个值。
  2. 写一个 PrintItems<T>,能打印任意类型的列表。
  3. 写一个 Result<T>,表示操作成功或失败。
  4. 分别用 stringintdecimal 测试。

作业拆解

按这个顺序写:

  1. 先写 Box<T>
  2. 创建 Box<string>Box<int>Box<decimal>
  3. PrintItems<T>
  4. List<string>List<int> 测试。
  5. Result<T>
  6. PrintResult<T> 输出成功或失败。

完整代码

var nameBox = new Box<string>("Keyboard"); var countBox = new Box<int>(3); var priceBox = new Box<decimal>(199m); Console.WriteLine(nameBox.Value.ToUpper()); Console.WriteLine(countBox.Value + 1); Console.WriteLine($"{priceBox.Value:F2}"); var names = new List<string> { "Keyboard", "Mouse" }; var scores = new List<int> { 80, 95, 62 }; PrintItems(names); PrintItems(scores); var success = new Result<decimal>(true, 199m, ""); var failure = new Result<decimal>(false, 0m, "价格格式不正确"); PrintResult(success); PrintResult(failure); void PrintItems<T>(List<T> items) { foreach (T item in items) { Console.WriteLine(item); } } void PrintResult<T>(Result<T> result) { if (result.Success) { Console.WriteLine($"成功: {result.Data}"); } else { Console.WriteLine($"失败: {result.Error}"); } } class Box<T> { public T Value { get; } public Box(T value) { Value = value; } } class Result<T> { public bool Success { get; } public T Data { get; } public string Error { get; } public Result(bool success, T data, string error) { Success = success; Data = data; Error = error; } }

正确运行结果

你应该能看到:

KEYBOARD 4 199.00 Keyboard Mouse 80 95 62 成功: 199 失败: 价格格式不正确

你要能说清楚这 6 件事

  • T 是类型占位符,不是固定类型。
  • Box<int> 里的 Tint
  • Box<string> 里的 Tstring
  • 泛型比 object 更安全,因为编译阶段能检查类型。
  • 泛型方法可以复用同一套逻辑。
  • 泛型约束用来限制 T 必须具备某些条件。