1. 为什么说 C# 集合是每个 .NET 开发者的必修课先聊一个我踩过的坑。刚转 C# 那会儿我写代码处理一批订单数据图省事全塞 ArrayList 里遍历时各种强转、拆箱代码又臭又长不说还时不时冒出一个 InvalidCastException。后来一位老同事看了我的代码丢下一句话你根本没用对集合。那之后我才意识到C# 集合这套东西看着简单用好了是效率利器用不好就是连环坑。C# 集合本质上是 .NET 框架提供的一系列数据结构用来存储、组织、检索和操作一组对象。它的核心价值在于你不需要自己手写数组扩容、链表插入、哈希查找这些底层逻辑直接挑一个合适的集合类型就能干活。而且泛型引入之后集合从“装一堆 object”进化成了“强类型容器”类型安全有了、性能也上来了代码可读性更是直线提升。这篇文章我打算把 C# 集合全体系拆开讲清楚从最基础的数组、List、Dictionary到 HashSet、Queue、Stack、LinkedList再到并发集合和不可变集合每个类型我都会说清楚它是什么、适合什么场景、怎么用、有什么坑。不管你是刚入门 C# 的新手还是写了好几年业务代码想系统梳理一遍的开发者这篇文章都能帮你把集合这块的知识补全。有一点先说明我不会像 MSDN 那样把每个 API 都列一遍重点关注的是“什么时候该用它、什么时候不该用它”。理解了这层你才算真正玩转了 C# 集合。2. 集合家族全景图先认清每个成员的定位2.1 数组、List 与 ArrayList从“死板”到“灵活”数组是所有集合的根基它在内存中是连续分配的通过索引访问元素的速度极快——O(1) 时间复杂度的随机访问是它最大的优势。但它的缺点也很明显长度固定声明时就必须确定大小。你没法往一个已经创建的数组里塞更多元素只能新建更大的数组然后把旧数据复制过去。实际开发中如果数据量是确定的、只读的比如一周七天的名称、一年的月份列表直接用数组是最干净的选择string[] weekDays { 周一, 周二, 周三, 周四, 周五, 周六, 周日 }; for (int i 0; i weekDays.Length; i) { Console.WriteLine(weekDays[i]); }List 就是数组的“升级版”。它在内部其实仍然是一个数组但当你 Add 元素导致容量不够时它会自动扩容——通常是按当前容量的两倍来扩然后复制旧数据到新数组。这个扩容机制决定了 List 的 Add 操作均摊时间复杂度接近 O(1)但如果频繁触发扩容会有一定的性能损耗。那 ArrayList 又是哪来的它是 .NET 1.0 时代的产物内部存储的是 object。也就是说你把一个 int 放进去它会被装箱成 object取出来用的时候又得拆箱回 int。装箱拆箱的性能开销不小而且类型安全性差你完全可以往同一个 ArrayList 里塞字符串、整数、对象取出来时完全不知道该强转成什么类型。现在.NET 生态里除非是在维护老代码或者做某些反射、序列化等需要 object 容器的场景否则一律建议用 List 代替 ArrayList。// 不推荐类型不安全 装箱拆箱 ArrayList list new ArrayList(); list.Add(42); list.Add(hello); int x (int)list[0]; // 推荐强类型无装箱性能更好 Listint numbers new Listint(); numbers.Add(42);2.2 Dictionary 与哈希用空间换时间的典型代表DictionaryTKey, TValue 是 C# 里最常用的哈希表实现底层依赖键的哈希码来组织数据。当你插入一个键值对时它会先计算键的 GetHashCode()然后根据哈希值确定存储位置查找时同样计算哈希码直接定位到目标位置附近因此理想情况下查找、插入、删除的时间复杂度都是 O(1)。这里有个关键点经常被忽略作为键的类型必须正确重写 Equals 和 GetHashCode。具体规则是——如果两个对象 Equals 返回 true那么它们的 GetHashCode 必须返回相同的值。反过来说如果两个对象的哈希码相等Equals 不一定返回 true这被称为哈希冲突Dictionary 内部用链表或开放寻址法来处理冲突。实际开发中业务对象的默认哈希实现引用相等往往不满足需求。比如你希望按订单编号来去重或查找订单那就得自己写一个键类型或者干脆用字符串、int 这类值类型做键省心很多Dictionarystring, int stockMap new Dictionarystring, int(); stockMap[apple] 10; stockMap[banana] 5; if (stockMap.TryGetValue(apple, out int count)) { Console.WriteLine($苹果库存{count}); }用 TryGetValue 而不是直接用索引器访问是为了避免键不存在时抛出 KeyNotFoundException。这是一个高频坑点我下面专门整理在常见问题里。2.3 Set 家族什么时候用 HashSet什么时候用 SortedSetHashSet 是一个不包含重复元素的集合同样基于哈希表实现所以添加、删除、查找元素的时间复杂度约等于 O(1)。它的典型使用场景是去重和集合运算比如交集、并集、差集。假设你在做一个用户标签系统要找出同时具有“VIP”和“活跃用户”两个标签的用户或者要合并两个渠道进来的用户名单HashSet 就非常顺手HashSetint setA new HashSetint { 1, 2, 3, 4 }; HashSetint setB new HashSetint { 3, 4, 5, 6 }; setA.IntersectWith(setB); // setA { 3, 4 }交集 setA.UnionWith(setB); // 并集 setA.ExceptWith(setB); // 差集SortedSet 和 HashSet 类似也是不重复元素的集合但内部用红黑树实现元素会按自然顺序或你指定的比较器排序。它的插入、删除、查找是 O(log n)比 HashSet 慢但好处是遍历时能得到有序序列。如果你需要有序去重存储比如按分数排序的排行榜去重SortedSet 是合适的选择。2.4 队列与栈先进先出和后进先出的两个极端Queue 是先进先出FIFO的集合就像排队买奶茶先来的人先拿。它非常适合任务调度的场景比如按顺序处理待办消息、广度优先搜索的节点遍历。常用方法就是 Enqueue入队和 Dequeue出队Peek 用于查看队首元素但不移除。Stack 则是后进先出LIFO就像一叠盘子后放上去的反而先被拿走。它适合用来实现撤销操作、括号匹配检测、深度优先搜索等。常用方法是 Push压栈、Pop弹栈、Peek查看栈顶。我举一个实际例子在做表达式解析时比如判断一串括号是否合法用 Stack 是最直接高效的思路public static bool IsValidBrackets(string input) { Stackchar stack new Stackchar(); foreach (char c in input) { if (c is ( or [) { stack.Push(c); } else if (c is ) or ]) { if (stack.Count 0) return false; char top stack.Pop(); if ((c ) top ! () || (c ] top ! [)) return false; } } return stack.Count 0; }2.5 LinkedList 与双向链表的设计取舍LinkedList 是双向链表每个节点都知道自己的前驱和后继因此从头部或尾部插入、删除元素的时间复杂度是 O(1)。但它的随机访问是 O(n)——你得从头或尾一个个遍历才能找到中间某个位置的元素。日常开发里LinkedList 的使用频率远低于 List 和 Dictionary但它有不可替代的场景。比如你需要频繁在序列中间插入或删除节点——模拟一个播放列表在正在播放的歌曲后面随时插入下一首LinkedList 就很合适。相比之下List 在中间插入需要把后面的元素全部后移代价很高。不过我得实话实说绝大多数业务场景中List 够用了而且 List 的缓存友好性更好内存连续遍历性能往往优于 LinkedList。LinkedList 用不好反而会带来更多的内存分配和更差的局部性。别为了“炫技”而强行上链表。3. 深入原理泛型、迭代器与内存分配那些事3.1 泛型集合为什么性能更好泛型的关键好处在生产“专用代码”上。List 和 ArrayList 相比前者在运行时就是直接存储 int 值后者存储的是装箱后的 object 引用。值类型在装箱时会分配在托管堆上导致额外的内存分配 引用层间接访问 GC垃圾回收压力增加。这些都是真实存在的性能成本在高频调用的代码路径里尤其明显。引用类型方面的差异稍微小一些但泛型集合还能提供编译期类型检查杜绝运行时类型转换异常。比如 ArrayList 里你拿出来的对象是 Cat 还是 Dog只有运行时才知道但 List 在编译时就锁死了。从框架层面看.NET 的泛型是“真泛型”不是仅仅把 object 换了个名字。它在 JIT 编译阶段会为每个值类型参数生成专门的本地代码避免了装箱和类型检查的开销。所以从 C# 2.0 引入泛型之后老式的非泛型集合类ArrayList、Hashtable、SortedList 等基本就被判了“死刑”。3.2 理解迭代器模式foreach 背后的秘密面试的时候经常问“foreach 和 for 有什么区别”这个话题也跟集合相关。foreach 本质上是对迭代器模式的语法糖。C# 编译器会把 foreach 展开成这样的逻辑using (var enumerator collection.GetEnumerator()) { while (enumerator.MoveNext()) { var item enumerator.Current; // 循环体 } }这带来两个关键好处第一你不需要关心索引、长度等细节代码更简洁第二它可以作用于任何实现了 IEnumerable / IEnumerable 的类型包括自定义集合。但用 foreach 的时候有个坑如果你在 foreach 循环体内修改集合Add 或 Remove会抛出 InvalidOperationException。因为大部分集合内部维护了一个 _version 字段每次修改都会递增迭代器在 MoveNext 时会检查版本号是否变化如果变了就立即抛异常。这就是“集合已修改可能无法执行枚举操作”这个经典报错的来源。3.3 容量、扩容与内存分配策略List 内部的数组有一个 Capacity 属性它表示当前数组能容纳的元素个数而 Count 才是实际元素个数。当你 Add 引发 Count 超过 Capacity 时List 会创建一块新数组通常容量变成原来的 2 倍然后把旧元素复制过去。这个过程会产生大量的内存拷贝和临时对象。如果你事先知道数据规模建议用带初始容量的构造函数Listint bigList new Listint(100000);这样可以大幅减少扩容次数提升性能。同样DictionaryTKey, TValue 也支持在构造时指定容量比如var dict new Dictionarystring, int(1000);这等于提前告诉字典“赶紧把桶bucket数量分配好别让我一次次重建内部结构”。我在处理几十万行数据的 Excel 导入时如果不指定容量程序能明显卡顿指定容量后导入速度提升非常显著。所以一句话拿不准规模的时候至少给一个大致的预估值。4. 并发场景下的集合选择线程安全没那么简单4.1 加锁集合与并发集合的本质差异早期 .NET 提供了 SynchronizedCollection、Hashtable 的 SyncRoot 等方式来做线程安全集合它们的做法是在内部的每个操作上都加锁。这种做法虽然保证单个操作的原子性但对复合操作比如先判断再删除仍然无能为力。后来 .NET 4.0 推出了 System.Collections.Concurrent 命名空间下的并发集合核心设计思路是“细粒度并发”和“无锁或低锁实现”ConcurrentDictionaryTKey, TValue多线程环境下安全地读写字典支持 TryAdd、TryUpdate、GetOrAdd、AddOrUpdate 这些原子操作。ConcurrentQueue 线程安全队列生产者消费者模式的首选。ConcurrentStack 线程安全栈。ConcurrentBag 一个无序、允许重复元素的线程安全集合适合“各干各的再汇总”的场景。BlockingCollection 包装器提供阻塞和限流能力配合生产者消费者非常方便。4.2 生产者-消费者经典模式实例举一个典型的后台任务采集场景一个线程负责从硬件设备读取实时数据生产者另一个线程负责把数据写入数据库或展示到界面消费者。用 ConcurrentQueue 连接两者写起来干净利落ConcurrentQueuestring queue new ConcurrentQueuestring(); // 生产者线程 Task.Run(() { for (int i 0; i 1000; i) { queue.Enqueue($数据-{i}); Thread.Sleep(10); } }); // 消费者线程 Task.Run(() { while (true) { if (queue.TryDequeue(out string item)) { Console.WriteLine($处理: {item}); } else { Thread.Sleep(5); } } });这里有一个容易忽略的点使用普通 List 加 lock 也能实现类似功能但在高并发下锁竞争会非常激烈性能明显下降。ConcurrentQueue 内部使用分段无锁队列设计吞吐量在多数场景下远超“一个大锁包住所有操作”的方案。4.3 该用同步集合还是并发集合的判断标准不是所有多线程场景都必须上并发集合。如果你的数据量不大、操作频率不高用 lock 包裹一个 List 也未尝不可代码更简单、更好维护。但如果是高频读写、对吞吐量和延迟有要求并发集合几乎是唯一合理选择。另外特别提醒并发集合保证的是“单个操作”的线程安全它并不保证“一系列操作”的原子性。如果你需要先检查存在、再修改数据这类复合逻辑还是需要自己加锁或用 ConcurrentDictionary 的原子方法TryUpdate、AddOrUpdate 等。5. 不可变集合与内存效率函数式风格带来的改变5.1 不可变集合到底解决了什么System.Collections.Immutable 命名空间提供了 ImmutableArray 、ImmutableList 、ImmutableDictionaryTKey, TValue、ImmutableHashSet 等类型。它们的核心特征是任何修改操作都不会改动原集合而是返回一个修改后的新集合。原集合保持不变所以可以安全地共享给多个线程不需要额外加锁。这种设计在配置系统、规则引擎、多线程共享状态等场景中特别有价值。比如应用配置在启动时加载一次后续多个线程并发读取用不可变集合就能彻底避免“读取时被修改”的问题。var config new Dictionarystring, string { [host] localhost, [port] 8080 }.ToImmutableDictionary(); // 任何地方读取都安全 string host config[host];5.2 不可变集合的性能代价与取舍不可变集合并不是零成本的。ImmutableList 用树形结构实现修改操作时间复杂度是 O(log n)远不如 List 的 O(1) 快。ImmutableArray 稍好它是对数组的包装修改会创建新数组。所以性能敏感的迭代场景或者修改极其频繁的代码路径里用不可变集合是不明智的。但在读取远多于写入、共享并发读取多的场景下它能省去大量锁同步开销整体收益反而更高。开发中你大概率不需要到处都用不可变集合但要清楚它的存在以及适合用在哪里关键时刻能解决并发设计上的大麻烦。5.3 只读与不可变的混淆点很多新手会把 ReadOnlyCollection 和真正不可变集合搞混。ReadOnlyCollection 只是包了一层“只读视图”底层指向的还是同一个可变集合。如果有人持有原始 List 的引用依然能修改数据。真正的不可变集合则是从根本上保证“谁都无法修改”。这在做 API 设计时很重要返回 ReadOnlyCollection 只能“防君子”真正想做到线程安全的数据共享还是得上 Immutable 集合或者返回副本。6. 选择困难症指南不同场景到底该用哪个集合说到最后很多人最关心的其实是“给我一个选择表”。我整理了一份基于多年开发经验的决策表场景需求推荐集合理由固定数量、频繁随机访问数组T[]内存连续O(1) 随机访问零额外开销动态长度、尾部增删List灵活、自动扩容、遍历性能良好需要频繁在中间插入/删除LinkedList插入删除 O(1)键值映射、按 key 快速查找DictionaryTKey, TValueO(1) 查找前提是键的哈希实现正确去重 快速查找HashSet唯一元素O(1) 操作有序去重SortedSet红黑树自动排序先进先出任务队列QueueFIFO后进先出回溯/撤销StackLIFO多线程生产者消费者ConcurrentQueue 或 BlockingCollection线程安全、高性能多线程安全字典ConcurrentDictionaryTKey, TValue高并发下更安全更高效只读共享ImmutableArray 或 ImmutableDictionaryTKey, TValue线程安全、不可变这张表是我在实际项目里反复验证过的不是从文档里抄出来的。选型的思考顺序是先考虑数据形态线性键值集合再考虑操作模式增删改查的频率最后考虑并发需求。三者都定下来集合类型基本就呼之欲出了。7. 实操分享一个上位机数据采集场景中的集合应用7.1 场景背景我做过一个 C# 上位机项目需要从多路传感器实时采集数据然后按照时间窗口聚合、显示、存储。这个场景对集合的要求非常典型既要高频写入又要定期读取统计还得保证数据不会在多个线程之间串线。一开始我以为用 List 加锁就行结果压力测试时发现 UI 线程频繁卡顿。后来我重新梳理了数据流采用了三个集合组合的方案ConcurrentQueue 负责接收原始数据。生产线程传感器驱动回调把数据入队处理线程从队列里消费数据天然形成缓冲不会丢失。Dictionarystring, List 按传感器 ID 分组存储数据。处理线程从队列取出每条数据后追加到对应传感器的 List 里方便后续统计。ConcurrentDictionarystring, SensorSnapshot 缓存最新快照实时界面刷新直接读它避免每次都要遍历全部历史数据。7.2 关键代码架构简化后的伪代码结构大致这样// 原始数据队列 private ConcurrentQueueSensorData _rawQueue new(); // 按传感器分组的最近窗口数据 private Dictionarystring, ListSensorData _groupedData new(); // 最新值缓存UI 直接读取 private ConcurrentDictionarystring, SensorSnapshot _latest new(); private void SensorCallback(SensorData data) { _rawQueue.Enqueue(data); } private void ProcessingLoop() { while (_running) { while (_rawQueue.TryDequeue(out var data)) { lock (_groupLocker) { if (!_groupedData.TryGetValue(data.SensorId, out var list)) { list new ListSensorData(); _groupedData[data.SensorId] list; } list.Add(data); } _latest[data.SensorId] new SensorSnapshot(data); } Thread.Sleep(1); } }这里我特别使用 ConcurrentQueue 作为唯一的生产者消费者通道处理线程做数据分组和缓存更新。注意 _groupedData 依然是普通 Dictionary因为对它访问时只用 lock 保护而 _latest 是并发字典UI 线程和数据处理线程都在读写它用并发集合方便很多。7.3 实际调优心得经历了这个项目之后我对“如何用集合才高效”有了很深的体会。总结下来几个关键点都是文档里不会明说的第一不要在 UI 线程里遍历大数据集否则界面必然卡死。把集合操作放到后台线程去UI 线程只读最新快照。第二ConcurrentQueue 的 Enqueue 和 TryDequeue 是线程安全的你不需要再加额外的锁。但如果你在多个生产者往同一个队列写数据的同时消费者还要查询队列长度这个长度可能只是近似值因为数据在飞时刻在变。第三List 的容量扩容是翻倍式的如果你知道每天大概会有多少条数据最好初始化时直接给个大容量避免处理高峰期的扩容风暴。第四分组统计时千万别在 foreach 里反复用 LINQ 的 Where 去过滤数据时间复杂度直接拉满。正确做法是维护一个按 key 索引的分组字典让查询变成 O(1)。8. 常见问题与避坑速查表8.1 高频报错与解决方案先列几个我见过的、几乎每个 C# 开发者都会遇到的高频问题问题原因解决方案Dictionary 抛出 KeyNotFoundException索引器读取不存在的键使用 TryGetValue 或 ContainsKey 预判InvalidOperationException: 集合已修改foreach 循环中修改集合使用 ToList() 先复制或在 for 循环中操作List 扩容导致 GC 压力频繁 Add 且容量不足构造时指定预估容量ArrayList 装箱拆箱性能差旧式非泛型集合换成 ListConcurrentDictionary 取值时出现“意外旧值”复合操作不是原子的使用 TryUpdate、GetOrAdd 或自行加锁HashSet 自定义类型不去重未重写 Equals / GetHashCode正确重写这两者或实现 IEqualityComparer8.2 添加元素时的高频错误误用索引器Dictionary 的索引器在键不存在时会抛出异常很多人第一步就踩这个坑。正确做法是if (!dict.TryAdd(key, value)) { // 键已存在处理冲突 }也可以先 TryGetValue 拿到已有值再决定是更新还是跳过。场景不同取舍不同但目标都是避免无意义的异常抛出。8.3 数组转 List、List 转数组的细节这两者互换非常常用但细节决定成败int[] array { 1, 2, 3 }; Listint list array.ToList(); Listint backToArray new Listint { 4, 5, 6 }; int[] arr backToArray.ToArray();ToArray / ToList 都会创建全新对象如果在大循环里反复转换会带来大量内存分配。能直接用 IEnumerable 的地方就不要转来转去最典型的就是 foreach 和 LINQ 链式调用。8.4 排序和查找的隐藏陷阱List 的 Sort 方法默认使用元素的 IComparable 实现对于自定义类如果没实现正确排序结果可能不是你想要的。用 LINQ 的 OrderBy 更容易写对但要注意它不会改变原集合只是返回一个新的有序序列。查找时还要注意区分“找索引”和“找元素”。List 的 IndexOf 用默认相等比较器FindIndex 则接受 Predicate 委托两者语义不同别用混。IndexOf 找的是“和给定对象相等的元素”FindIndex 找的是“满足条件的元素”。举个例子你想找列表中第一个大于 10 的元素的位置必须用 FindIndexIndexOf 做不到。9. 进阶玩法自定义集合与扩展方法9.1 扩展方法让集合操作更优雅C# 的扩展方法非常适合扩展集合的操作体验。比如你经常会从一个集合里随机取一个元素写成扩展方法就很顺手public static class CollectionExtensions { public static T RandomElementT(this IListT list) { if (list null || list.Count 0) throw new ArgumentException(集合不能为空); int index Random.Shared.Next(list.Count); return list[index]; } } // 调用 var item myList.RandomElement();还有批量添加的扩展方法一次性把多个元素 AddRange 进来或者在添加前先做个去重判断。这些封装能让业务代码更干净也更便于复用。9.2 实现自定义集合记住 IEnumerable 与 ICollection当你需要自定义集合类型比如一个日志队列、一个带提醒的字典大概率需要实现 IEnumerable 以便支持 foreach实现 ICollection 以便拥有 Count、Add、Remove 等方法。一个基本骨架如下public class CustomCollectionT : IEnumerableT { private readonly ListT _items new(); public void Add(T item) _items.Add(item); public IEnumeratorT GetEnumerator() _items.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() GetEnumerator(); }别小看这个骨架很多中间件、框架里的集合扩展就是从这个起点开始的。核心思维永远是你要告诉调用方“这个集合怎么用”而不是把内部细节全部暴露出去。9.3 与 LINQ 的结合集合操作的艺术LINQ 是把集合操作提升到新高度的关键。Where、Select、GroupBy、OrderBy 这种链式写法让数据处理变得像流水线一样直观。举一个实际场景从一堆订单里提取出金额大于 1000 的订单并按用户分组统计总额var result orders .Where(o o.Amount 1000) .GroupBy(o o.UserId) .Select(g new { UserId g.Key, TotalAmount g.Sum(x x.Amount) }) .OrderByDescending(x x.TotalAmount) .ToList();这段代码放在以前非 LINQ 时代得写一堆循环和中间变量。现在一行链式调用就完成了可读性还很高。但也要警惕滥用LINQ 在某些情况下会产生额外的中间集合分配性能敏感的高频循环里不如手动实现来得快。10. 常用操作速查开发中随用随查的清单整理一份最常用的集合操作速查方便你日常开发直接翻10.1 List 常用操作var list new Listint(); list.Add(1); // 添加到末尾 list.AddRange(new[] { 2, 3 }); // 批量添加 list.Insert(0, 0); // 指定索引插入 list.Remove(2); // 删除第一个值为2的元素 list.RemoveAt(0); // 按索引删除 list.RemoveAll(x x 2); // 按条件删除 list.Contains(3); // 是否包含 list.IndexOf(3); // 查找索引 list.Find(x x 1); // 查找第一个满足条件的元素 list.FindAll(x x 1); // 找出所有满足条件的元素 list.Sort(); // 排序 list.Reverse(); // 反转10.2 Dictionary 常用操作var dict new Dictionarystring, int(); dict.Add(key1, 1); // 添加 dict.TryAdd(key2, 2); // 安全添加 dict[key1] 10; // 更新 dict.TryGetValue(key1, out int val); // 安全读取 dict.Remove(key1); // 删除 dict.ContainsKey(key2); // 判断键存在 dict.ContainsValue(10); // 判断值存在性能较差慎用 dict.Keys; // 获取所有键 dict.Values; // 获取所有值10.3 HashSet 常用操作var set new HashSetint { 1, 2, 3 }; set.Add(4); // 添加如果已存在则返回 false set.Remove(2); // 移除 set.Contains(3); // 判断存在 set.UnionWith(new[] { 4, 5 }); // 并集 set.IntersectWith(new[] { 3, 4 }); // 交集 set.ExceptWith(new[] { 3 }); // 差集 set.SymmetricExceptWith(new[] { 5 }); // 对称差集10.4 Queue 与 Stack 常用操作var queue new Queueint(); queue.Enqueue(1); queue.Enqueue(2); int head queue.Peek(); // 查看队首不移除 int first queue.Dequeue(); // 取出队首并移除 var stack new Stackint(); stack.Push(1); stack.Push(2); int top stack.Peek(); // 查看栈顶 int popped stack.Pop(); // 弹出栈顶并移除这些操作不需要死记写多了自然就熟。关键是能意识到还有哪些方法可用遇事不要总是绕远路。11. 我的实践总结与最后建议做了这么多年 .NET 开发我最大的感受是集合选型不是一件可以“临时拍脑袋”的事。产品的性能瓶颈、代码的可维护性、并发场景下的稳定性往往在你写下第一行集合代码时就已注定。选错了集合后面修修补补的成本非常高。我给新人的建议是先熟练掌握 List、Dictionary、HashSet 这三个最常用的类型把它们的增删改查和常见坑位背熟。然后逐步拓展到 Queue、Stack、LinkedList以及并发集合和不可变集合。不要一上来就追求“掌握所有类型”而是要在实际场景中反复体会“为什么这种场景用这个集合更好”。如果你正打算系统整理自己的 C# 集合知识我建议自己画一张脑图把每个集合的核心特征、适用场景、常见坑位都列出来项目里遇到相关需求时就翻一翻。这套动作坚持一段时间你会发现自己的代码质量和定位问题的速度都跟以前不一样了。最后分享一个小技巧新写的代码里优先考虑用“接口或者抽象类型”来暴露集合比如 IReadOnlyList 、IEnumerable 而不要直接返回 List 。这能让你后续替换实现比如从 List 换成 ImmutableArray时不必改动调用方。好的集合使用习惯会让你的代码在未来的很长一段时间里都保持灵活和健壮。