Skip to Content
Week 02Day 1 - 集合体系

Day 1 - 集合体系

建议用时:180-210 分钟

你将学会什么

  • 为什么除了 List,还需要其他集合
  • ListDictionaryHashSetQueueStack 分别解决什么问题
  • 什么时候按顺序保存,什么时候按 key 查找,什么时候自动去重
  • Dictionary 的 key 和 value 是什么
  • 为什么读取 Dictionary 时优先用 TryGetValue
  • Queue 的先进先出和 Stack 的后进先出怎么理解
  • 如何根据需求选择合适集合

本页仍然是 C# 基础。不要把集合名字当成要背的单词,要把它们和具体问题对应起来:顺序、查找、去重、排队、撤销。

本页固定顺序

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

学习衔接

上一页学习的是“小项目:记账 CLI”,今天继续学习“集合体系”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。

今天的最低通过线

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

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

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

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

今天先抓住什么

集合不是只有 List。不同集合对应不同问题:

要保留顺序和重复:List 要根据编号快速查数据:Dictionary 要自动去重:HashSet 要按先来后到处理:Queue 要做撤销或回退:Stack

选集合时,先问问题,再选类型。

名词先讲清楚

名词直接解释例子
List有顺序、允许重复的一组数据商品列表
Dictionary用 key 快速找到 value商品编号找价格
key查找用的键"A001"
valuekey 对应的值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 数量: 3

List 不会帮你去重。你加了两次 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 New

HashSet 适合判断“有没有出现过”和“要不要去重”。

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顺序清楚,容易遍历
根据编号快速查数据Dictionarykey 查 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] = valuekey 存在就覆盖
尝试添加dict.TryAdd(key, value)key 已存在返回 false
安全查找dict.TryGetValue(key, out value)推荐读取方式
判断 keydict.ContainsKey(key)只判断是否存在
删除dict.Remove(key)删除指定 key
遍历 keydict.Keys只看所有 key
遍历 valuedict.Values只看所有 value
遍历键值foreach (var item in dict)item.Keyitem.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 角色数量: 3

Queue<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 导入商品 1

Peek() 只是看一眼,Dequeue() 会真的取走。

常见错误和修法

错误原因修法
List 反复按编号查找每次都要遍历改用 Dictionary
直接 dict[key] 读取不存在的 keykey 不存在会报错TryGetValue
以为 HashSet 保留重复数据HashSet 会去重需要重复就用 List
Queue 直接 Dequeue队列没有元素先判断 Count > 0
Stack 直接 Pop栈没有元素先判断 Count > 0

今天写代码时按这个顺序

  1. 先写清楚要解决的问题。
  2. 判断是否需要顺序和重复。
  3. 判断是否需要按 key 查找。
  4. 判断是否需要去重。
  5. 判断是否需要先进先出或后进先出。
  6. 选择集合后,用 3 条数据先跑通。
  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}"); } } 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());

运行结果或界面效果:

启动电机 读取温度

改动任务: 再加入“停止电机”,观察先进先出。

第四部分:作业完整答案

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

作业要求

写一个商品集合练习程序,要求:

  1. List<string> 保存原始商品名称,允许重复。
  2. HashSet<string> 统计不重复商品。
  3. Dictionary<string, decimal> 保存商品价格。
  4. Queue<string> 保存待处理任务。
  5. Stack<string> 保存操作历史。
  6. 输出每种集合的结果。

作业拆解

按这个顺序写:

  1. 先准备原始商品列表。
  2. 用原始列表创建 HashSet
  3. 创建价格字典。
  4. 遍历去重后的商品名,并查价格。
  5. 创建任务队列,逐个出队。
  6. 创建操作历史,逐个撤销。

完整代码

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 是后进先出。