Day 4 - 泛型
建议用时:180-210 分钟
你将学会什么
- 泛型是什么,为什么
List<int>和List<string>都叫List T是什么,为什么它只是类型占位符- 泛型和
object的区别 - 泛型方法怎么写
- 泛型类怎么写
- 泛型约束
where的基本用法 - 如何用泛型写一个可复用的盒子、打印方法和结果类型
泛型不是高级魔法。先把它理解成“把类型也当成参数传进去”,这样同一套代码就能安全处理不同类型。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“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,都有 Add、Count、foreach,只是里面装的数据类型不同。
名词先讲清楚
| 名词 | 直接解释 | 例子 |
|---|---|---|
| 泛型 | 让类型变成可传入参数的写法 | List<T> |
| 类型参数 | 类型的占位符 | T |
| 具体类型 | 真正传进去的类型 | int、string |
| 泛型类 | 带类型参数的类 | 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 : class | T 必须是引用类型 |
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 : class | T 必须是引用类型 | 只接受类对象 |
where T : struct | T 必须是值类型 | 只接受数字、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。
今天写代码时按这个顺序
- 先看有没有重复逻辑。
- 判断重复点是不是只有类型不同。
- 把具体类型换成
T。 - 调用时传入具体类型,例如
Box<int>。 - 如果需要
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()。
常见错误和修法
| 错误 | 为什么错 | 修法 |
|---|---|---|
| 泛型参数名随便写 | 读代码看不出含义 | 常用 T、TItem、TResult、TKey |
| 泛型方法里调用不存在的属性 | 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 添加字符串,观察编译错误。
第四部分:作业完整答案
这一部分给出当天作业的完整答案。先按第三部分做一遍,再用这里检查。
作业要求
写一个泛型练习程序,要求:
- 写一个
Box<T>,能保存任意类型的一个值。 - 写一个
PrintItems<T>,能打印任意类型的列表。 - 写一个
Result<T>,表示操作成功或失败。 - 分别用
string、int、decimal测试。
作业拆解
按这个顺序写:
- 先写
Box<T>。 - 创建
Box<string>、Box<int>、Box<decimal>。 - 写
PrintItems<T>。 - 用
List<string>和List<int>测试。 - 写
Result<T>。 - 写
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>里的T是int。Box<string>里的T是string。- 泛型比
object更安全,因为编译阶段能检查类型。 - 泛型方法可以复用同一套逻辑。
- 泛型约束用来限制
T必须具备某些条件。