集合类

.NET Standard标准中规定了一系列通用集合类,包括大部分常见的数据结构,如线性表、字典、集合、栈、队列等。我们实际开发中,基本不需要再手写这些结构,而应该尽量采用标准库的实现,避免重复造轮子。这里要注意的是.NET Standard只是一个标准,它没有规定底层运行时如何实现这些数据结构,但由于这些都是极为基础的数据结构,无论是.NET Core还是Mono其实现应该是类似的。

System.Collections.Generic 通用泛型集合工具包

System.Collections.Generic包含了一系列用于定义泛型集合的接口和类,允许用户创建强类型集合。常用的有如下几种:

  • List 顺序表
  • LinkedList 链表
  • Dictionary 字典
  • SortedDictionary 有序字典
  • HashSet 集合
  • SortedSet 有序集合
  • Stack 栈
  • Queue 队列

线性表

线性表包含顺序表(数据结构中也常被称为动态数组)和链表,顺序表通常基于定长数组实现,但额外实现了扩充机制,具有较高的随机访问性能。

// 创建顺序表
List<string> list = new List<string>();

// 顺序插入元素
list.Add("apple");
list.Add("banana");
list.Add("pear");

// 随机插入元素
list.Insert(1, "orange");

// 删除元素
list.Remove("pear");

// 根据索引获取元素
string s = list[1];
Console.WriteLine(s);

// 遍历顺序表
foreach (string item in list)
{
    Console.WriteLine(item);
}

上面例子代码中,简单演示了顺序表List<T>中的一些操作。下面整理了List<T>中其它一些极为常用的成员。

成员 说明
Count 属性,获取元素个数,注意区别数组使用Length
Contains 判断是否包含指定元素
IndexOf 返回指定元素的索引,找不到返回-1
Find / FindAll 按条件查找第一个 / 全部匹配的元素,参数是一个返回bool的委托(Predicate<T>)
Sort / Reverse 排序 / 反转
AddRange 一次性添加多个元素
RemoveAt 按索引删除元素
Clear 清空所有元素

Find等按条件操作的成员配合Lambda表达式使用非常方便,下面是一个例子。

List<int> list = new List<int> { 1, 2, 3, 4, 5 };
int found = list.Find(n => n > 3);        // 4,第一个大于3的元素
List<int> all = list.FindAll(n => n > 3); // 4, 5

如果声明集合的同时初始化其中的元素,可以使用以下语法:

List<string> list = new List<string>
{
    "apple", "banana", "pear"
};

字典

字典分为无序字典和有序字典,无序字典通常采用哈希表实现,插入和搜索性能极高,唯一的缺点是哈希表是无法指定顺序的;而有序字典通常采用二叉搜索树实现(如红黑树),搜索性能比哈希表会低一些,但支持指定顺序。此外我们要知道的一点是,字典的键不能重复,但要注意两种添加方式的行为差异:使用Add()添加重复的键会抛出ArgumentException异常;而使用索引器dict[key] = value赋值时,若键已存在则会覆盖对应的值,若键不存在则直接添加。

// 创建字典
Dictionary<string, string> dict = new Dictionary<string, string>();

// 插入数据
dict.Add("title", "my diary");
dict.Add("content", "hello world");
dict.Add("date", "1994-12-17");

// 通过Key获取值
string s = dict["title"];
Console.WriteLine(s);

// 删除键
dict.Remove("date");

// 遍历Key
foreach (string key in dict.Keys)
{
    Console.WriteLine("key {0}", key);
}

// 遍历字典键值对
foreach (KeyValuePair<string, string> item in dict)
{
    Console.WriteLine("key {0} value {1}", item.Key, item.Value);
}

上面例子演示了哈希字典Dictionary<K,V>的使用。这里还有几点需要特别注意:

  1. 通过索引器dict["title"]读取键对应的值时,如果键不存在会抛出KeyNotFoundException异常。实际开发中更推荐使用TryGetValue()方法,它在键不存在时返回false而不抛异常,并且只需一次查找就能同时完成判断和取值,性能也更好。
if (dict.TryGetValue("title", out string? value))
{
    Console.WriteLine(value);
}
  1. 如果只需要判断键是否存在而不关心值,可以使用ContainsKey()方法。
  2. 无序字典基于哈希表实现,遍历时的元素顺序是不保证的,不要依赖遍历顺序编写逻辑,需要有序遍历时应使用SortedDictionary<K,V>或对键排序后再遍历。

集合

集合和字典的实现类似,也分为无序集合和有序集合两种,分别基于哈希表和二叉搜索树。此外,集合和线性表的区别是集合中不能有重复的元素。

// 创建集合
HashSet<string> set = new HashSet<string>();

// 添加元素
set.Add("apple");
set.Add("banana");
set.Add("pear");

// 删除元素
set.Remove("apple");

// 遍历集合
foreach (string s in set)
{
    Console.WriteLine(s);
}

上面例子演示了哈希集合HashSet<T>的使用。Add()方法返回bool值表示元素是否被真正添加(集合中已存在相同元素时返回false),Contains()方法可以判断元素是否存在。此外,作为数学意义上的集合,HashSet<T>还提供了一系列集合运算方法。

方法 说明
UnionWith 并集,将另一个集合的元素合并进来
IntersectWith 交集,只保留两个集合都存在的元素
ExceptWith 差集,移除另一个集合中存在的元素
IsSubsetOf / IsSupersetOf 判断是否为另一个集合的子集 / 超集
HashSet<int> a = new HashSet<int> { 1, 2, 3 };
HashSet<int> b = new HashSet<int> { 2, 3, 4 };
a.IntersectWith(b); // a 变为 { 2, 3 }

栈

栈结构一般都是基于线性表实现,栈具有后进先出(LIFO)的特性。

// 创建栈
Stack<string> stack = new Stack<string>();

// 元素入栈
stack.Push("apple");
stack.Push("banana");
stack.Push("pear");

// 获取栈顶元素
string peek = stack.Peek();
Console.WriteLine(peek);

// 元素出栈
string pop = stack.Pop();
Console.WriteLine(pop);

// 获取栈高度
int length = stack.Count;
Console.WriteLine(length);

上面例子演示了栈Stack<T>的使用。需要注意的是,在空栈上调用Peek()或Pop()会抛出InvalidOperationException异常,如果不确定栈是否为空,可以使用TryPeek()和TryPop()方法,它们在栈为空时返回false而不抛异常,用法和字典的TryGetValue()类似。

队列

队列一般也都是基于线性表实现,和栈相反,队列具有先进先出(FIFO)的特性。

// 创建队列
Queue<string> queue = new Queue<string>();

// 元素存入队列尾部
queue.Enqueue("apple");
queue.Enqueue("banana");
queue.Enqueue("pear");

// 获取队列头部元素
string peek = queue.Peek();
Console.WriteLine(peek);

// 队列头部元素出队列
string top = queue.Dequeue();
Console.WriteLine(top);

// 获取队列长度
int length = queue.Count;
Console.WriteLine(length);

上面例子演示了队列Queue<T>的使用。和栈一样,在空队列上调用Peek()或Dequeue()会抛出InvalidOperationException异常,可以使用TryPeek()和TryDequeue()方法避免。

其它集合功能

除了上面介绍的几种最常用的集合类,System.Collections.Generic中还有一些特定场景下使用的集合:

  • SortedList<K,V>:有序字典的另一种实现,它基于数组而非二叉搜索树,元素占用内存更紧凑,但插入删除时需要移动元素,适合创建后读取多、修改少的场景。
  • PriorityQueue<TElement, TPriority>(.NET 6新增):优先队列,出队时按优先级从小到大弹出元素,常用于任务调度等场景。

除此之外,System.Collections中的是旧版的不支持泛型的集合类,它们的存在只是为了向后兼容,不推荐使用。System.Collections.Concurrent中则包含了一些线程安全的集合类,用于多线程环境使用,相关内容将在并发编程章节详细介绍,这里就不展开演示了。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。