Day 1 - 集合体系
建议用时:180-210 分钟
你将学会什么
- 为什么除了
List,还需要其他集合 List、Dictionary、HashSet、Queue、Stack分别解决什么问题- 什么时候按顺序保存,什么时候按 key 查找,什么时候自动去重
Dictionary的 key 和 value 是什么- 为什么读取
Dictionary时优先用TryGetValue Queue的先进先出和Stack的后进先出怎么理解- 如何根据需求选择合适集合
本页仍然是 C# 基础。不要把集合名字当成要背的单词,要把它们和具体问题对应起来:顺序、查找、去重、排队、撤销。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“小项目:记账 CLI”,今天继续学习“集合体系”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 3 项,就可以继续:
- 能用自己的话说明“集合体系”解决什么问题。
- 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
- 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。
第一部分:先学原理和最小知识
这一部分先解决“这是什么、为什么要这样写”。每看到一个短例子就亲手敲一遍并运行,不要先背完整程序。
今天先抓住什么
集合不是只有 List。不同集合对应不同问题:
要保留顺序和重复:List
要根据编号快速查数据:Dictionary
要自动去重:HashSet
要按先来后到处理:Queue
要做撤销或回退:Stack选集合时,先问问题,再选类型。
名词先讲清楚
| 名词 | 直接解释 | 例子 |
|---|---|---|
List | 有顺序、允许重复的一组数据 | 商品列表 |
Dictionary | 用 key 快速找到 value | 商品编号找价格 |
| key | 查找用的键 | "A001" |
| value | key 对应的值 | 199m |
HashSet | 不允许重复的一组数据 | 标签、角色、唯一名称 |
Queue | 先进先出 | 排队任务 |
Stack | 后进先出 | 撤销记录 |
Add | 添加元素 | list.Add("A") |
TryGetValue | 尝试按 key 查找 | 查到返回 true |
Enqueue | 入队 | 加到队尾 |
Dequeue | 出队 | 从队头取出 |
Push | 压栈 | 放到栈顶 |
Pop | 弹栈 | 从栈顶取出 |
List:保留顺序,允许重复
var names = new List<string>();
names.Add("Keyboard");
names.Add("Mouse");
names.Add("Keyboard");
foreach (string name in names)
{
Console.WriteLine(name);
}
Console.WriteLine($"数量: {names.Count}");输出:
Keyboard
Mouse
Keyboard
数量: 3List 不会帮你去重。你加了两次 Keyboard,里面就有两个 Keyboard。
Dictionary:用 key 找 value
Dictionary<TKey, TValue> 可以理解成一张查找表。
Key Value
A001 -> Keyboard
B002 -> Mouse
C003 -> Cable代码:
var products = new Dictionary<string, string>();
products["A001"] = "Keyboard";
products["B002"] = "Mouse";
products["C003"] = "Cable";
if (products.TryGetValue("B002", out string? name))
{
Console.WriteLine(name);
}
else
{
Console.WriteLine("商品不存在");
}输出:
Mouse为什么不要直接用中括号读取
下面这样能读到数据:
var prices = new Dictionary<string, decimal>
{
["Keyboard"] = 199m
};
Console.WriteLine(prices["Keyboard"]);但如果 key 不存在:
prices["Mouse"]程序会报错。业务代码里经常不确定 key 是否存在,所以优先用:
var prices = new Dictionary<string, decimal>
{
["Keyboard"] = 199m
};
if (prices.TryGetValue("Mouse", out decimal price))
{
Console.WriteLine(price);
}
else
{
Console.WriteLine("没有找到价格");
}HashSet:自动去重
var tags = new HashSet<string>();
tags.Add("Hot");
tags.Add("New");
tags.Add("Hot");
Console.WriteLine($"标签数量: {tags.Count}");
foreach (string tag in tags)
{
Console.WriteLine(tag);
}输出:
标签数量: 2
Hot
NewHashSet 适合判断“有没有出现过”和“要不要去重”。
Queue:先进先出
Queue 像排队。先进去的先出来。
var tasks = new Queue<string>();
tasks.Enqueue("导入商品");
tasks.Enqueue("计算库存");
tasks.Enqueue("生成报表");
Console.WriteLine(tasks.Dequeue());
Console.WriteLine(tasks.Dequeue());输出:
导入商品
计算库存Queue 适合任务队列、消息处理、排队叫号。
Stack:后进先出
Stack 像一摞盘子,最后放上去的先拿出来。
var history = new Stack<string>();
history.Push("输入商品名");
history.Push("修改价格");
history.Push("保存商品");
Console.WriteLine(history.Pop());
Console.WriteLine(history.Pop());输出:
保存商品
修改价格Stack 适合撤销、回退、浏览路径。
集合怎么选
| 需求 | 选什么 | 原因 |
|---|---|---|
| 保存一组数据,允许重复 | List | 顺序清楚,容易遍历 |
| 根据编号快速查数据 | Dictionary | key 查 value 更直接 |
| 自动去重 | HashSet | 重复元素不会加进去 |
| 先来的任务先处理 | Queue | 先进先出 |
| 最近的操作先撤销 | Stack | 后进先出 |
集合常用操作速查
List<T> 常用操作
| 需求 | 写法 | 说明 |
|---|---|---|
| 添加 | list.Add(item) | 加到末尾 |
| 插入 | list.Insert(index, item) | 插到指定位置 |
| 删除值 | list.Remove(item) | 删除第一个匹配值 |
| 删除位置 | list.RemoveAt(index) | 删除指定下标 |
| 清空 | list.Clear() | 删除全部元素 |
| 判断包含 | list.Contains(item) | 简单值很常用 |
| 查下标 | list.IndexOf(item) | 找不到返回 -1 |
| 排序 | list.Sort() | 改变原列表 |
| 反转 | list.Reverse() | 改变原列表 |
| 数量 | list.Count | 判断空列表 |
Dictionary<TKey, TValue> 常用操作
| 需求 | 写法 | 说明 |
|---|---|---|
| 添加或覆盖 | dict[key] = value | key 存在就覆盖 |
| 尝试添加 | dict.TryAdd(key, value) | key 已存在返回 false |
| 安全查找 | dict.TryGetValue(key, out value) | 推荐读取方式 |
| 判断 key | dict.ContainsKey(key) | 只判断是否存在 |
| 删除 | dict.Remove(key) | 删除指定 key |
| 遍历 key | dict.Keys | 只看所有 key |
| 遍历 value | dict.Values | 只看所有 value |
| 遍历键值 | foreach (var item in dict) | item.Key 和 item.Value |
| 数量 | dict.Count | 键值对数量 |
完整例子:
var prices = new Dictionary<string, decimal>();
prices["Keyboard"] = 199m;
prices["Mouse"] = 59m;
if (!prices.TryAdd("Keyboard", 299m))
{
Console.WriteLine("Keyboard 已经存在,不重复添加");
}
if (prices.TryGetValue("Mouse", out decimal mousePrice))
{
Console.WriteLine($"Mouse: {mousePrice:F2}");
}
foreach (string key in prices.Keys)
{
Console.WriteLine($"商品名: {key}");
}HashSet<T> 常用操作
| 需求 | 写法 | 说明 |
|---|---|---|
| 添加 | set.Add(item) | 成功返回 true,重复返回 false |
| 判断包含 | set.Contains(item) | 去重和权限判断常用 |
| 删除 | set.Remove(item) | 删除指定值 |
| 清空 | set.Clear() | 删除全部 |
| 并集 | set.UnionWith(other) | 合并两组 |
| 交集 | set.IntersectWith(other) | 保留两组都有的 |
| 差集 | set.ExceptWith(other) | 删除另一组里有的 |
| 数量 | set.Count | 不重复数量 |
完整例子:
var roles = new HashSet<string> { "User", "Editor" };
Console.WriteLine(roles.Add("Editor"));
Console.WriteLine(roles.Add("Admin"));
Console.WriteLine(roles.Contains("Admin"));
Console.WriteLine($"角色数量: {roles.Count}");输出:
False
True
True
角色数量: 3Queue<T> 和 Stack<T> 常用操作
| 集合 | 需求 | 写法 | 说明 |
|---|---|---|---|
Queue | 入队 | queue.Enqueue(item) | 加到队尾 |
Queue | 查看下一个 | queue.Peek() | 不移除 |
Queue | 出队 | queue.Dequeue() | 取出并移除 |
Queue | 数量 | queue.Count | 空队列不能 Dequeue |
Stack | 入栈 | stack.Push(item) | 放到栈顶 |
Stack | 查看栈顶 | stack.Peek() | 不移除 |
Stack | 出栈 | stack.Pop() | 取出并移除 |
Stack | 数量 | stack.Count | 空栈不能 Pop |
Peek() 和 Dequeue() 的区别:
var queue = new Queue<string>();
queue.Enqueue("导入商品");
queue.Enqueue("计算库存");
Console.WriteLine(queue.Peek());
Console.WriteLine(queue.Count);
Console.WriteLine(queue.Dequeue());
Console.WriteLine(queue.Count);输出:
导入商品
2
导入商品
1Peek() 只是看一眼,Dequeue() 会真的取走。
常见错误和修法
| 错误 | 原因 | 修法 |
|---|---|---|
用 List 反复按编号查找 | 每次都要遍历 | 改用 Dictionary |
直接 dict[key] 读取不存在的 key | key 不存在会报错 | 用 TryGetValue |
以为 HashSet 保留重复数据 | HashSet 会去重 | 需要重复就用 List |
空 Queue 直接 Dequeue | 队列没有元素 | 先判断 Count > 0 |
空 Stack 直接 Pop | 栈没有元素 | 先判断 Count > 0 |
今天写代码时按这个顺序
- 先写清楚要解决的问题。
- 判断是否需要顺序和重复。
- 判断是否需要按 key 查找。
- 判断是否需要去重。
- 判断是否需要先进先出或后进先出。
- 选择集合后,用 3 条数据先跑通。
- 给找不到、空集合、重复数据补处理。
第二部分:把知识组合成完整例子
前面已经学过最小知识。现在把它们组合起来,先读懂执行顺序,再完整敲一遍。今天最终要写一个商品集合小程序:保留原始列表、去重、按编号查价格、处理任务队列。
最终你要写出这个程序
var productNames = new List<string> { "Keyboard", "Mouse", "Keyboard", "Cable" };
var uniqueNames = new HashSet<string>(productNames);
var prices = new Dictionary<string, decimal>
{
["Keyboard"] = 199m,
["Mouse"] = 59m,
["Cable"] = 19m
};
Console.WriteLine($"原始商品数量: {productNames.Count}");
Console.WriteLine($"去重后数量: {uniqueNames.Count}");
foreach (string name in uniqueNames)
{
if (prices.TryGetValue(name, out decimal price))
{
Console.WriteLine($"{name}: {price:F2}");
}
}
var tasks = new Queue<string>();
tasks.Enqueue("导入商品");
tasks.Enqueue("计算库存");
tasks.Enqueue("生成报表");
Console.WriteLine($"下一个任务: {tasks.Dequeue()}");输出:
原始商品数量: 4
去重后数量: 3
Keyboard: 199.00
Mouse: 59.00
Cable: 19.00
下一个任务: 导入商品这个例子里,每种集合负责不同任务:List 保留原始数据,HashSet 去重,Dictionary 查价格,Queue 按顺序处理任务。
第三部分:跟着敲代码
从这里开始动手。每个例子都可以直接放进 Program.cs 运行。
例子 1:List 保留原始顺序
var names = new List<string> { "Keyboard", "Mouse", "Keyboard" };
for (int i = 0; i < names.Count; i++)
{
Console.WriteLine($"{i + 1}. {names[i]}");
}预期输出:
1. Keyboard
2. Mouse
3. Keyboard例子 2:Dictionary 按编号查商品
var products = new Dictionary<string, string>
{
["A001"] = "Keyboard",
["B002"] = "Mouse",
["C003"] = "Cable"
};
Console.Write("商品编号: ");
string? code = Console.ReadLine();
if (code != null && products.TryGetValue(code.Trim(), out string? name))
{
Console.WriteLine($"商品名称: {name}");
}
else
{
Console.WriteLine("商品不存在");
}输入 B002 时输出 Mouse。输入不存在的编号时输出“商品不存在”。
例子 3:Dictionary 保存价格
var prices = new Dictionary<string, decimal>
{
["Keyboard"] = 199m,
["Mouse"] = 59m,
["Cable"] = 19m
};
foreach (var item in prices)
{
Console.WriteLine($"{item.Key}: {item.Value:F2}");
}item.Key 是商品名,item.Value 是价格。
例子 4:HashSet 去重
var rawNames = new List<string> { "Keyboard", "Mouse", "Keyboard", "Cable", "Mouse" };
var uniqueNames = new HashSet<string>(rawNames);
Console.WriteLine($"原始数量: {rawNames.Count}");
Console.WriteLine($"去重数量: {uniqueNames.Count}");
foreach (string name in uniqueNames)
{
Console.WriteLine(name);
}HashSet 不保证一定按你输入顺序展示,所以不要依赖它的顺序。
例子 5:Queue 任务队列
var tasks = new Queue<string>();
tasks.Enqueue("导入商品");
tasks.Enqueue("计算库存");
tasks.Enqueue("生成报表");
while (tasks.Count > 0)
{
string task = tasks.Dequeue();
Console.WriteLine($"处理任务: {task}");
}输出顺序和入队顺序一样。
例子 6:Stack 撤销记录
var history = new Stack<string>();
history.Push("输入商品名称");
history.Push("修改价格");
history.Push("保存商品");
while (history.Count > 0)
{
string action = history.Pop();
Console.WriteLine($"撤销: {action}");
}最后保存的动作最先被撤销。
例子 7:同一批数据用三种集合处理
var productNames = new List<string> { "Keyboard", "Mouse", "Keyboard", "Cable" };
var uniqueNames = new HashSet<string>(productNames);
var prices = new Dictionary<string, decimal>
{
["Keyboard"] = 199m,
["Mouse"] = 59m,
["Cable"] = 19m
};
Console.WriteLine($"原始商品数量: {productNames.Count}");
Console.WriteLine($"去重后数量: {uniqueNames.Count}");
foreach (string name in uniqueNames)
{
if (prices.TryGetValue(name, out decimal price))
{
Console.WriteLine($"{name}: {price:F2}");
}
}这个例子要看集合分工:List 保存原始输入,HashSet 去重,Dictionary 查价格。
小白重复敲写训练
下面三段不要复制粘贴。第一遍照着敲,第二遍关掉页面重写,第三遍换一组数据。
训练 1:List 添加、删除、遍历
var products = new List<string>();
products.Add("Keyboard");
products.Add("Mouse");
products.Add("Monitor");
products.Remove("Mouse");
foreach (string product in products)
{
Console.WriteLine(product);
}改动任务:再添加 Cable,然后输出 products.Count。
训练 2:Dictionary 按编号查名称
var names = new Dictionary<string, string>();
names.Add("P001", "Keyboard");
names.Add("P002", "Mouse");
if (names.TryGetValue("P002", out string? name))
{
Console.WriteLine(name);
}改动任务:查询不存在的 P999,补一个 else 输出“没有找到”。
训练 3:HashSet 自动去重
var tags = new HashSet<string>();
tags.Add("new");
tags.Add("sale");
tags.Add("new");
Console.WriteLine(tags.Count);先预测结果,再运行。想清楚为什么添加三次却不是三项。
每日小测
做完本页后,用这 5 题检查是否真的掌握。
1. 判断题
本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。
答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。
2. 填空题
本页主题是:集合体系。今天至少要掌握的 3 个点是:
1. 为什么除了 `List`,还需要其他集合
2. `List`、`Dictionary`、`HashSet`、`Queue`、`Stack` 分别解决什么问题
3. 什么时候按顺序保存,什么时候按 key 查找,什么时候自动去重答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。
3. 流程题
遇到本页相关功能时,先按什么顺序处理?
答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。
4. 找错误题
如果本页代码运行失败,第一步应该做什么?
答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。
5. 改需求题
在本页完整例子跑通后,至少改一个小需求。
可选改法:
- 改一个字段名称。
- 多加一个校验条件。
- 多输出一行结果。
- 把固定数据改成用户输入。
- 把一次处理改成多条数据处理。
答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:为什么除了 List,还需要其他集合。
上位机专项练习
不同集合对应不同上位机问题:设备列表需要顺序,设备编号需要快速查找,命令需要排队,报警编号需要去重。
下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。
专项例子 1:List 保存在线设备
var devices = new List<string> { "PLC-01", "仪表-02" };
devices.Add("传感器-03");
foreach (string device in devices)
{
Console.WriteLine(device);
}运行结果或界面效果:
PLC-01
仪表-02
传感器-03改动任务: 删除“仪表-02”再运行。
专项例子 2:Dictionary 按编号查状态
var statuses = new Dictionary<string, string>
{
["PLC-01"] = "在线",
["PLC-02"] = "离线"
};
if (statuses.TryGetValue("PLC-02", out string? status))
{
Console.WriteLine(status);
}运行结果或界面效果:
离线改动任务: 查找一个不存在的 PLC-99,并补上 else 提示。
专项例子 3:Queue 排队发送命令
var commands = new Queue<string>();
commands.Enqueue("启动电机");
commands.Enqueue("读取温度");
Console.WriteLine(commands.Dequeue());
Console.WriteLine(commands.Dequeue());运行结果或界面效果:
启动电机
读取温度改动任务: 再加入“停止电机”,观察先进先出。
第四部分:作业完整答案
这一部分给出当天作业的完整答案。先按第三部分做一遍,再用这里检查。
作业要求
写一个商品集合练习程序,要求:
- 用
List<string>保存原始商品名称,允许重复。 - 用
HashSet<string>统计不重复商品。 - 用
Dictionary<string, decimal>保存商品价格。 - 用
Queue<string>保存待处理任务。 - 用
Stack<string>保存操作历史。 - 输出每种集合的结果。
作业拆解
按这个顺序写:
- 先准备原始商品列表。
- 用原始列表创建
HashSet。 - 创建价格字典。
- 遍历去重后的商品名,并查价格。
- 创建任务队列,逐个出队。
- 创建操作历史,逐个撤销。
完整代码
var productNames = new List<string> { "Keyboard", "Mouse", "Keyboard", "Cable", "Mouse" };
var uniqueNames = new HashSet<string>(productNames);
var prices = new Dictionary<string, decimal>
{
["Keyboard"] = 199m,
["Mouse"] = 59m,
["Cable"] = 19m
};
Console.WriteLine("=== 商品价格 ===");
Console.WriteLine($"原始数量: {productNames.Count}");
Console.WriteLine($"去重数量: {uniqueNames.Count}");
foreach (string name in uniqueNames)
{
if (prices.TryGetValue(name, out decimal price))
{
Console.WriteLine($"{name}: {price:F2}");
}
else
{
Console.WriteLine($"{name}: 没有价格");
}
}
var tasks = new Queue<string>();
tasks.Enqueue("导入商品");
tasks.Enqueue("计算库存");
tasks.Enqueue("生成报表");
Console.WriteLine("=== 任务队列 ===");
while (tasks.Count > 0)
{
Console.WriteLine($"处理: {tasks.Dequeue()}");
}
var history = new Stack<string>();
history.Push("添加 Keyboard");
history.Push("修改 Mouse 价格");
history.Push("删除 Cable");
Console.WriteLine("=== 撤销顺序 ===");
while (history.Count > 0)
{
Console.WriteLine($"撤销: {history.Pop()}");
}正确运行结果
你应该能看到:
原始数量是 5
去重数量是 3
商品能输出价格
任务按导入、计算、生成的顺序处理
撤销按删除、修改、添加的反向顺序处理你要能说清楚这 5 件事
List适合保留原始顺序和重复数据。Dictionary适合根据 key 快速查 value。HashSet适合去重和判断是否存在。Queue是先进先出。Stack是后进先出。