C#集合:从基础到进阶的全面解析
C#集合:从基础到进阶的全面解析
在 C# 编程中,集合是处理数据集合的核心工具。无论是存储一组对象、实现缓存机制,还是处理复杂的数据结构,都离不开集合的灵活运用。本文将全面深入地探讨 C# 集合体系,从基础概念到高级技巧,帮助开发者掌握集合的精髓,写出更高效、更优雅的代码。
一、集合概述与分类
C# 集合框架是.NET
类库的重要组成部分,它提供了一系列用于存储和操作数据的类和接口。与数组相比,集合具有动态扩容、类型安全(泛型集合)、丰富操作方法等优势,是处理可变数据量场景的首选。
1. 泛型与非泛型集合
C# 集合分为泛型集合和非泛型集合两大类:
- 非泛型集合:位于
System.Collections
命名空间,如ArrayList
、Hashtable
等。它们存储object
类型,存在装箱拆箱操作,性能较差,且缺乏编译时类型检查,现在已逐渐被泛型集合取代。 - 泛型集合:位于
System.Collections.Generic
命名空间,如List<T>
、Dictionary<TKey, TValue>
等。通过泛型参数指定元素类型,避免了装箱拆箱,提供类型安全,是现代 C# 开发的主流选择。
2. 集合接口体系
集合框架基于接口构建,核心接口包括:
IEnumerable<T>
:支持泛型迭代,定义了GetEnumerator()
方法,是所有可迭代集合的基础。ICollection<T>
:继承自IEnumerable<T>
,增加了元素数量、添加、删除等操作。IList<T>
:继承自ICollection<T>
,提供索引访问能力,如List<T>
实现了该接口。IDictionary<TKey, TValue>
:定义键值对集合的操作,如Dictionary<TKey, TValue>
。
二、常用集合类型详解
1. List:动态数组的首选
List<T>
是最常用的集合类型,内部通过数组实现,支持动态扩容。
// 初始化与基本操作
var fruits = new List<string>();
fruits.Add("Apple");
fruits.AddRange(new[] { "Banana", "Cherry" });
fruits.Insert(1, "Orange");// 便利
foreach (var fruit in fruits)
{Console.WriteLine(fruit);
}// 查找与过滤
var result = fruits.Find(f => f.StartsWith("A"));
var filtered = fruits.FindAll(f => f.Length > 5);
内部原理:List<T>
初始容量为 4,当元素数量超过容量时,会创建一个新的、容量为原来 2 倍的数组,并复制旧元素,因此频繁扩容会影响性能。建议初始化时指定初始容量(如new List<string>(100)
),减少扩容次数。
2. Dictionary<TKey, TValue>:键值对高效映射
Dictionary<TKey, TValue>
基于哈希表实现,提供 O (1) 的平均查找效率,适用于通过键快速访问值的场景。
var studentScores = new Dictionary<int, int>
{{ 101, 90 },{ 102, 85 }
};// 添加与访问
studentScores.Add(103, 95);if (studentScores.TryGetValue(102, out int score))
{Console.WriteLine($"Score: {score}");
}// 遍历键值对
foreach (var kvp in studentScores)
{Console.WriteLine($"Key: {kvp.Key}, Value: {kvp.Value}");
}
哈希冲突处理:当两个键的哈希码相同时,Dictionary
通过链表将冲突的元素存储在同一桶中,此时查找复杂度退化为 O (n)。因此,应确保键类型实现了良好的GetHashCode
和Equals
方法。
3. HashSet:高性能去重集合
HashSet<T>
是无序集合,不允许重复元素,基于哈希表实现,添加、删除、包含操作的平均时间复杂度为 O (1)。
var uniqueNumbers = new HashSet<int> { 1, 2, 3 };
uniqueNumbers.Add(2); // 重复元素,添加失败
uniqueNumbers.Remove(3);bool contains = uniqueNumbers.Contains(1); // 高效判断包含
适用场景:去重、集合运算(如交集IntersectWith
、并集UnionWith
)。
4. Queue与 Stack:特殊访问模式集合
-
Queue:先进先出(FIFO)集合,适用于消息队列、任务调度等场景。
var queue = new Queue<string>(); queue.Enqueue("First"); queue.Enqueue("Second");var item = queue.Dequeue(); // 取出"First"
-
Stack:后进先出(LIFO)集合,适用于表达式求值、撤销操作等。
var stack = new Stack<int>(); stack.Push(1); stack.Push(2);var top = stack.Pop(); // 取出2
5. LinkedList:链表结构的灵活实现
LinkedList<T>
是双向链表,元素通过节点链接,插入和删除元素(已知节点位置时)效率高(O (1)),但随机访问效率低(O (n))。
var linkedList = new LinkedList<string>();
var node = linkedList.AddFirst("First");
linkedList.AddAfter(node, "Second");
适用场景:需要频繁在集合中间插入 / 删除元素的场景。
三、性能分析与选择策略
不同集合类型的性能特性差异显著,选择合适的集合是优化代码的关键。
集合类型 | 查找效率 | 插入 / 删除效率(中间位置) | 适用场景 |
---|---|---|---|
List<T> | O(1) | O(n) | 动态数组、随机访问频繁 |
Dictionary<,> | O(1) | O (1)(平均) | 键值对映射、快速查找 |
HashSet<T> | O(1) | O (1)(平均) | 去重、集合运算 |
LinkedList<T> | O(n) | O (1)(已知节点) | 频繁插入 / 删除中间元素 |
Queue<T> | O(n) | O (1)(队尾) | FIFO 场景,如消息队列 |
Stack<T> | O(n) | O (1)(栈顶) | LIFO 场景,如撤销操作 |
选择策略:
- 根据核心操作(查找、插入、删除)的频率选择。
- 考虑数据是否有序、是否允许重复。
- 预估数据量,初始化时指定合适容量(如
List<T>
、Dictionary<,>
)。
四、线程安全与并发集合
多线程环境下,普通集合(如List<T>
、Dictionary<,>
)不是线程安全的,并发操作可能导致数据异常。.NET 提供了专门的并发集合(System.Collections.Concurrent
命名空间):
ConcurrentBag<T>
:无序的线程安全集合,适合多线程添加和获取元素。ConcurrentDictionary<TKey, TValue>
:线程安全的键值对集合。ConcurrentQueue<T>
/ConcurrentStack<T>
:线程安全的队列和栈。
// 并发字典示例
var concurrentDict = new ConcurrentDictionary<int, string>();
concurrentDict.TryAdd(1, "One");
if (concurrentDict.TryUpdate(1, "NewOne", "One"))
{// 更新成功
}
使用建议:多线程读写时优先使用并发集合,避免手动加锁的复杂性。
五、最佳实践与高级技巧
1. 优先使用泛型集合
非泛型集合(如ArrayList
)会导致装箱拆箱操作,影响性能且缺乏类型安全。例如:
// 不推荐:ArrayList存在装箱拆箱
ArrayList list = new ArrayList();
list.Add(1); // 装箱
int value = (int)list[0]; // 拆箱// 推荐:List<T>类型安全且无装箱
List<int> intList = new List<int>();
intList.Add(1);
int val = intList[0];
2. 利用接口编程
依赖抽象接口(如IEnumerable<T>
、ICollection<T>
)而非具体实现,提高代码灵活性。例如,方法参数使用IEnumerable<T>
,可接受任何实现该接口的集合:
public void ProcessItems(IEnumerable<string> items)
{foreach (var item in items){// 处理逻辑}
}
3. 集合初始化与 LINQ 结合
C# 支持集合初始化器和 LINQ 查询,简化集合操作:
// 集合初始化器
var numbers = new List<int> { 1, 2, 3, 4, 5 };// LINQ查询与集合转换
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();var numberDict = numbers.ToDictionary(n => n, n => n * 2);
4. 自定义集合
当系统集合无法满足需求时,可通过实现IEnumerable<T>
、ICollection<T>
等接口创建自定义集合:
public class ReadOnlyList<T> : IEnumerable<T>
{private readonly List<T> _innerList;public ReadOnlyList(List<T> list){_innerList = new List<T>(list);}public IEnumerator<T> GetEnumerator(){return _innerList.GetEnumerator();}IEnumerator IEnumerable.GetEnumerator(){return GetEnumerator();}
}
六、总结
C# 集合框架提供了丰富的类型和功能,从基础的List<T>
到复杂的并发集合,每种类型都有其独特的适用场景。掌握集合的内部原理、性能特性和最佳实践,能帮助开发者编写更高效、更健壮的代码。在实际开发中,应根据具体需求(如操作类型、数据量、线程环境)选择合适的集合类型,并充分利用泛型和 LINQ 提升开发效率。通过不断实践和深入理解,才能真正发挥 C# 集合的强大威力。