(一)
集合接口:
IEnumerable<T>
:
IEnumerable<T>
是用于表示一组元素的通用接口,它定义了一个方法
GetEnumerator()
,该方法返回一个枚举器,用于遍历集合中的元素。
ICollection<T>
:
ICollection<T>
接口扩展了
IEnumerable<T>
,并添加了一些用于管理集合的方法,如添加、移除和清除元素等。
IList<T>
:
IList<T>
接口扩展了
ICollection<T>
,并添加了通过索引访问元素的方法,以及插入、移除和查找元素的方法。
IDictionary<TKey, TValue>
:
IDictionary<TKey, TValue>
接口表示键值对的集合,它定义了用于管理键值对的方法,如添加、移除和查找键值对等。
(二)
集合类型:
数组(Array)
: 数组是一组相同类型的元素的集合,通过索引进行访问。数组的大小在创建时就确定,并且无法动态改变大小。
列表(List<T>)
:
List<T>
是一个动态数组,它可以根据需要自动增长或缩减大小。它提供了添加、删除、搜索和排序元素的方法。
链表(LinkedList<T>)
:
LinkedList<T>
是一个双向链表,每个节点包含一个值和指向前一个节点和后一个节点的引用。它提供了高效的插入和删除操作。
字典(Dictionary<TKey, TValue>)
:
Dictionary<TKey, TValue>
是一个键值对集合,用于存储唯一的键和对应的值。它提供了根据键快速查找值的功能。
集合(HashSet<T>)
:
HashSet<T>
是一个无序的集合,用于存储唯一的元素。它提供了高效的添加、删除和查找元素的操作。
队列(Queue<T>)
:
Queue<T>
是一个先进先出(FIFO)的集合,用于存储元素,并且提供了在队列的末尾添加元素以及从队列的开头移除元素的操作。
栈(Stack<T>)
:
Stack<T>
是一个后进先出(LIFO)的集合,用于存储元素,并且提供了在栈的顶部添加元素以及从栈的顶部移除元素的操作。
有序集合(SortedSet<T>)
:
SortedSet<T>
是一个有序的集合,用于存储唯一的元素,并且以排序的顺序进行存储。它提供了高效的添加、删除和查找元素的操作。
这些集合接口和类型提供了丰富的功能来满足不同的需求,你可以根据具体的场景和要求选择合适的集合类型。
二、
列表
在 C# 中,列表(List<T>)是一种动态数组,它允许我们在运行时动态地添加或移除元素,而无需事先指定容量。列表实现了
IList<T>
接口,提供了一系列方法来操作列表中的元素。
(一)
优点:
动态大小:
列表具有动态大小的特性,可以根据需要自动增长或缩减,不需要事先指定容量。
高效的随机访问:
列表支持通过索引进行快速随机访问元素,因为列表中的元素是连续存储的,因此可以直接通过索引来访问。
内置泛型支持:
List<T>
是一个泛型集合,它能够存储任意类型的元素,提高了代码的类型安全性和可重用性。
丰富的操作方法:
列表提供了丰富的操作方法,如添加、删除、插入、排序等,使得对列表中的元素进行操作变得非常简单。
(二)
缺点:
插入和删除效率较低:
列表在插入和删除元素时,如果涉及到移动其他元素,则效率较低,时间复杂度为 O(n)。特别是在列表的中间或开头插入或删除元素时,需要移动大量元素。
可能的额外内存开销:
列表的底层实现通常是数组,当列表的容量超过当前数组的大小时,可能会触发数组的重新分配和复制操作,导致一些额外的内存开销。
不适用于大规模数据的频繁插入和删除:
由于列表在插入和删除操作时的效率较低,因此不适用于需要频繁进行大规模数据的插入和删除操作的场景。
综上所述,列表在提供高效的随机访问和丰富的操作方法方面具有明显优势,但在插入和删除效率、内存
三、
队列
在 C# 中,队列(Queue<T>)是一种先进先出(FIFO)的数据结构,它是一种线性数据结构,可以存储一组元素,并且允许在队列的末尾添加元素,以及从队列的开头移除元素。队列的实现是基于链表或数组。
(一)
常用方法:
Enqueue(T item)
:将一个元素添加到队列的末尾。
Queue<int> queue = new Queue<int>();
queue.Enqueue(1);
queue.Enqueue(2);
Dequeue()
:移除并返回队列开头的元素。
Queue<int> queue = new Queue<int>() { 1, 2, 3 };
int item = queue.Dequeue(); // 移除并返回元素1
Peek()
:返回队列开头的元素,但不移除它。
Queue<int> queue = new Queue<int>() { 1, 2, 3 };
int item = queue.Peek(); // 返回元素1,队列不变
Count
:获取队列中的元素个数。
Queue<int> queue = new Queue<int>() { 1, 2, 3 };
int count = queue.Count; // 3
Clear()
:移除队列中的所有元素。
Queue<int> queue = new Queue<int>() { 1, 2, 3 };
queue.Clear(); // 清空队列
Contains(T item)
:检查队列是否包含指定的元素。
Queue<int> queue = new Queue<int>() { 1, 2, 3 };
bool contains = queue.Contains(2); // true
队列是一种常用的数据结构,用于管理需要按照先进先出顺序进行处理的元素。在许多情况下,队列都能够提供高效的操作,因此它是 C# 中的一个重要的集合类型。
(二)
优点:
简单易用:
Queue<T>
提供了简单的接口,使得在队列数据结构上执行常见操作变得非常简单。
高效的插入和移除操作:
队列是一种先进先出(FIFO)的数据结构,插入和移除操作的时间复杂度都是 O(1),因此在这些操作上具有高效性能。
内置泛型支持:
Queue<T>
是一个泛型集合,它能够存储任意类型的元素,提高了代码的类型安全性和可重用性。
适用于特定场景:
队列常用于需要先进先出顺序的问题,例如实现广度优先搜索、任务调度、消息传递等。
(三)
缺点:
无随机访问:
队列中的元素只能从队头插入和从队尾移除,而且无法直接通过索引访问中间的元素。这意味着队列不适用于需要随机访问的场景。
额外的内存开销:
队列中的元素通常以链表或数组的形式存储,这可能会导致一些额外的内存开销。特别是在存储大量元素时,可能会占用较多的内存。
不支持并发操作:
Queue<T>
是非线程安全的,如果需要在多个线程中并发访问队列,则需要进行额外的同步处理。
综上所述,队列在简单场景下提供了高效的插入和移除操作,但对于需要随机访问或并发操作的场景,可能不是最佳选择。在选择数据结构时,应根据具体的需求和场景来进行选择。
四、
堆栈
在 C# 中,堆栈(Stack<T>)是一种后进先出(LIFO)的数据结构,它是一种线性数据结构,允许在堆栈顶部添加元素,以及从堆栈顶部移除元素。堆栈的实现通常基于链表或数组。
(一)
特点:
后进先出:
堆栈中的元素按照后进先出的顺序进行添加和移除,即最后添加的元素最先被移除。
添加和移除操作:
堆栈提供了在堆栈顶部添加元素和从堆栈顶部移除元素的操作,这两个操作的时间复杂度都是 O(1)。
无索引访问:
堆栈中的元素无法直接通过索引访问,只能通过添加和移除元素的操作来处理堆栈中的元素。
(二)
常用方法:
Push(T item)
:将一个元素推入堆栈顶部。
Stack<int> stack = new Stack<int>();
stack.Push(1);
stack.Push(2);
Pop()
:从堆栈顶部移除并返回一个元素。
Stack<int> stack = new Stack<int>() { 1, 2, 3 };
int item = stack.Pop(); // 移除并返回元素3
Peek()
:返回堆栈顶部的元素,但不移除它。
Stack<int> stack = new Stack<int>() { 1, 2, 3 };
int item = stack.Peek(); // 返回元素3,堆栈不变
Count
:获取堆栈中的元素个数。
Stack<int> stack = new Stack<int>() { 1, 2, 3 };
int count = stack.Count; // 3
Clear()
:移除堆栈中的所有元素。
Stack<int> stack = new Stack<int>() { 1, 2, 3 };
stack.Clear(); // 清空堆栈
Contains(T item)
:检查堆栈是否包含指定的元素。
csharpCopy code
Stack<int> stack = new Stack<int>() { 1, 2, 3 };
bool contains = stack.Contains(2); // true
堆栈在许多场景下都是非常有用的数据结构,例如表达式求值、递归算法等。通过堆栈,可以轻松地实现后进先出的数据处理需求。
(三)
优点:
简单易用:
Stack<T>
提供了简单的接口,使得在堆栈数据结构上执行常见操作变得非常简单。
高效的插入和移除操作:
堆栈是一种后进先出(LIFO)的数据结构,插入和移除操作的时间复杂度都是 O(1),因此在这些操作上具有高效性能。
内置泛型支持:
Stack<T>
是一个泛型集合,它能够存储任意类型的元素,提高了代码的类型安全性和可重用性。
适用于特定场景:
堆栈常用于需要后进先出顺序的问题,例如实现递归算法、表达式求值、深度优先搜索等。
(四)
缺点:
无随机访问:
堆栈中的元素只能从顶部插入和移除,而且无法直接通过索引访问中间或底部的元素。这意味着堆栈不适用于需要随机访问的场景。
额外的内存开销:
堆栈中的元素通常以链表或数组的形式存储,这可能会导致一些额外的内存开销。特别是在存储大量元素时,可能会占用较多的内存。
不支持并发操作:
Stack<T>
是非线程安全的,如果需要在多个线程中并发访问堆栈,则需要进行额外的同步处理。
综上所述,
Stack<T>
在简单场景下提供了高效的插入和移除操作,但对于需要随机访问或并发操作的场景,可能不是最佳选择。在选择数据结构时,应根据具体的需求和场景来进行选择。
五、
链表
链表(LinkedList<T>)是一种基本的数据结构,在 C# 中的实现为
LinkedList<T>
,它代表一个双向链表,其中的每个节点都包含对前一个节点和后一个节点的引用。链表中的元素不必在内存中连续存储,这使得插入和删除操作非常高效。下面我们来深入解析链表的结构、特点和常用操作。
(一)
结构:
在
LinkedList<T>
中,每个节点由
LinkedListNode<T>
类型表示,它包含两个字段:
Value
:存储节点的值。
Next
:存储指向下一个节点的引用。
Previous
:存储指向前一个节点的引用。
链表本身则由一个头节点和一个尾节点组成,这两个节点分别表示链表的开始和结束。
(二)
常用操作:
AddFirst(T value)
:在链表的开头添加一个新节点。
AddLast(T value)
:在链表的末尾添加一个新节点。
AddBefore(LinkedListNode<T> node, T value)
:在指定节点之前插入一个新节点。
AddAfter(LinkedListNode<T> node, T value)
:在指定节点之后插入一个新节点。
Remove(LinkedListNode<T> node)
:移除指定节点。
RemoveFirst()
:移除链表的第一个节点。
RemoveLast()
:移除链表的最后一个节点。
Find(T value)
:查找链表中是否存在指定值的节点。
Clear()
:清空链表,移除所有节点。
Count
:获取链表中的节点个数。
(三)
示例:
LinkedList<int> linkedList = new LinkedList<int>();
// 添加元素
linkedList.AddLast(1);
linkedList.AddLast(2);
linkedList.AddLast(3);
// 插入元素
LinkedListNode<int> node = linkedList.Find(2);
linkedList.AddAfter(node, 4);
// 删除元素
linkedList.RemoveFirst();
linkedList.RemoveLast();
// 遍历链表
foreach (int value in linkedList)
Console.WriteLine(value);
链表在某些场景下比数组更加适用,尤其是涉及频繁的插入和删除操作时。通过链表,我们可以以较低的时间复杂度实现这些操作,从而提高程序的性能和效率。
链表(LinkedList<T>)作为一种数据结构,在某些情况下具有明显的优势,但在其他情况下可能会不如其他数据结构。以下是链表的优点和缺点:
(四) 优点:
高效的插入和删除操作: 由于链表的元素不必在内存中连续存储,因此在任意位置插入和删除元素的操作都非常高效,时间复杂度为 O(1)。
动态大小: 链表的大小可以根据需要动态增长或缩减,不像数组一样需要事先指定容量。
较小的空间开销: 链表只需要存储元素的值以及对前后节点的引用,因此在内存使用上比较高效。
支持双向遍历: 链表节点具有指向前一个节点和后一个节点的引用,因此可以很方便地支持双向遍历。
(五) 缺点:
无索引访问: 链表中的元素无法直接通过索引访问,只能通过遍历或使用特定的方法来访问和操作。这导致了在需要随机访问元素时效率较低。
额外的指针开销: 每个链表节点都需要存储指向前一个节点和后一个节点的引用,这会导致额外的指针开销,尤其是在存储大量数据时。
不适用于高频查找操作: 由于无法直接通过索引访问元素,链表不适合于需要频繁查找特定元素的场景,效率较低。
占用更多的内存: 相对于数组等连续存储的数据结构,链表可能会占用更多的内存,因为每个节点需要额外的指针来指向其他节点。
综上所述,链表在插入和删除操作频繁且不需要随机访问元素的场景下表现良好,但在需要频繁查找特定元素或需要随机访问元素的场景下效率较低。因此,在选择数据结构时需要根据具体的需求和场景来进行选择。
六、排序列表
在 C# 中对列表进行排序时,了解不同排序算法的工作原理以及它们的性能特征是很重要的。C# 中的 List<T> 类提供了 Sort 方法来进行排序,该方法通常使用基于快速排序的排序算法。下面是对排序列表的深入解析:
(一) 排序算法的选择:
快速排序(Quick Sort): 在大多数情况下,C# 的 List<T> 类使用快速排序算法进行排序。快速排序是一种高效的分而治之算法,具有平均时间复杂度为 O(n log n) 的性能。它在大多数情况下表现良好,并且对于大型数据集也具有良好的性能。
归并排序(Merge Sort): 在某些情况下,C# 的 List<T> 类会选择使用归并排序算法。归并排序也是一种分而治之的算法,具有稳定的 O(n log n) 时间复杂度。它对于大型数据集的排序性能较稳定,并且可以在外部排序中应用。
堆排序(Heap Sort): 虽然在 C# 中的 List<T> 类没有直接提供堆排序的实现,但堆排序也是一种高效的排序算法。堆排序具有 O(n log n) 的时间复杂度,但与快速排序和归并排序相比,它更适合于内存有限的情况下,因为它是一种原地排序算法,不需要额外的空间。
(二) 性能考虑:
数据集大小: 对于小型数据集,快速排序通常是首选,因为它在平均情况下具有较好的性能。但对于大型数据集,归并排序可能更适合,因为它具有稳定的 O(n log n) 时间复杂度。
数据的分布: 快速排序对于随机分布的数据通常表现良好,但对于已经近乎有序的数据集,其性能可能下降。在这种情况下,归并排序可能更合适,因为它对于已排序的子数组也能保持较好的性能。
稳定性: 如果需要保持相等元素的相对顺序不变,归并排序是一种稳定的排序算法,而快速排序通常是不稳定的。
(三) 自定义排序:
C# 中的 Sort 方法还允许通过提供自定义的比较器来进行排序,这允许我们根据特定的需求对列表进行排序。
List<string> words = new List<string>() { "apple", "banana", "orange" };
words.Sort((x, y) => x.Length.CompareTo(y.Length)); // 根据字符串长度排序
(四) 总结:
在对列表进行排序时,我们应该根据数据集的大小、分布特征以及排序的稳定性等因素来选择合适的排序算法。对于一般情况下的排序需求,C# 中的 List<T> 提供的排序方法通常已经足够满足需求,但在特定情况下,可能需要考虑使用自定义的比较器来实现排序。
(五) OrderBy VS Sort
下面是对比 OrderBy 方法和 Sort 方法的一些主要特点的表格展示:
(六) SortedList
SortedList<TKey, TValue> 是 C# 中的一种有序字典集合,它根据键的顺序自动对元素进行排序。以下是对 SortedList<TKey, TValue> 的详细解释:
1. 优点:
有序性:SortedList<TKey, TValue> 中的元素是按照键的顺序自动进行排序的,这使得查找操作更加高效,并且能够保持元素的有序性。
快速的查找操作: 由于元素是按顺序存储的,因此 SortedList<TKey, TValue> 提供了快速的查找操作。平均时间复杂度为 O(log n),其中 n 是 SortedList<TKey, TValue> 中的元素数量。
键值对集合:SortedList<TKey, TValue> 是一种键值对集合,每个键都与一个值相关联。它提供了对键值对集合进行高效管理的方法,如添加、删除、查找等。
动态大小:SortedList<TKey, TValue> 的大小可以根据需要动态增长或缩减,不需要事先指定容量。
2. 缺点:
低效的插入和删除操作: 由于需要维护元素的有序性,因此插入和删除操作的性能相对较低。特别是在中间或开头插入或删除元素时,需要移动其他元素。插入和删除操作的时间复杂度为 O(n)。
内存开销: 与其他集合类型相比,SortedList<TKey, TValue> 可能会占用更多的内存空间。这是因为它需要存储额外的索引信息来维护元素的有序性。
无法包含重复键:SortedList<TKey, TValue> 中的键必须是唯一的,因此无法添加重复的键。如果尝试将重复的键添加到 SortedList<TKey, TValue> 中,则会引发异常。
不支持自定义排序规则:SortedList<TKey, TValue> 默认按照键的自然顺序进行排序,或者可以通过提供比较器来指定排序规则。但是它不支持在运行时动态地更改排序规则。
七、字典
C# 中的字典(Dictionary<TKey, TValue>)是一种键值对集合,其中每个键都必须是唯一的,并且与一个值相关联。字典提供了高效的查找、插入和删除操作,是 C# 中常用的数据结构之一
(一) 优点:
高效的查找操作: 字典内部通常使用哈希表实现,这使得查找操作非常高效,平均时间复杂度为 O(1)。这意味着在字典中查找一个键所对应的值的速度非常快,即使字典中包含大量的键值对。
键值对关系: 字典是一种键值对集合,提供了一种方便的方式来存储和检索键值对关系。每个键都必须是唯一的,这使得字典非常适合于表示一对一的关联关系。
动态大小: 字典的大小可以根据需要动态增长或缩减,不需要事先指定容量。这使得字典在存储动态数据集时非常灵活。
丰富的操作方法: 字典提供了丰富的操作方法,如添加、删除、查找等,使得对字典中的键值对进行操作变得非常简单和灵活。
(二) 缺点:
无序集合: 字典中的元素不保持任何顺序,即它们不是按照插入顺序或按照键的顺序排列的。这可能使得对字典进行迭代时无法保证元素的顺序。
可能的哈希冲突: 哈希表在处理哈希冲突时可能会导致性能下降。虽然哈希表具有平均时间复杂度为 O(1) 的查找操作,但在极端情况下,哈希冲突可能会导致查找操作的时间复杂度为 O(n)。
内存开销: 字典通常会占用较多的内存空间,尤其是在存储大量键值对时。每个键值对都需要额外的内存来存储键和值的信息,这可能会增加内存开销。
不适用于有序操作: 字典不是有序集合,因此不适合于需要按照特定顺序访问元素的场景。如果需要有序的键值对集合,可能需要使用其他数据结构,如有序字典(SortedDictionary)。
八、集合
(一) HashSet<T>
HashSet<T> 是 C# 中的一种集合,用于存储唯一的元素。它提供了高效的查找、插入和删除操作,并且确保集合中不包含重复的元素。
(二) 优点:
唯一性:HashSet<T> 中的元素是唯一的,即集合中不允许包含重复的元素。这使得 HashSet<T> 很适合于需要存储一组唯一元素的场景。
高效的查找操作:HashSet<T> 使用哈希表来实现,因此提供了快速的查找操作。平均情况下,查找操作的时间复杂度为 O(1)。
快速的插入和删除操作:HashSet<T> 提供了快速的插入和删除操作。平均情况下,插入和删除操作的时间复杂度为 O(1)。
动态大小:HashSet<T> 的大小可以根据需要动态增长或缩减,不需要事先指定容量。这使得 HashSet<T> 在存储动态数据集时非常灵活。
(三) 缺点:
无序性:HashSet<T> 中的元素不保持任何顺序,即它们不是按照插入顺序或按照元素的顺序排列的。这可能使得对 HashSet<T> 进行迭代时无法保证元素的顺序。
内存开销: 与其他集合类型相比,HashSet<T> 可能会占用更多的内存空间。这是因为哈希表需要存储额外的索引信息来实现快速的查找操作。
哈希冲突: 尽管哈希表提供了快速的查找操作,但在处理哈希冲突时可能会导致性能下降。在极端情况下,哈希冲突可能会导致查找操作的时间复杂度为 O(n)。
不支持索引访问:HashSet<T> 中的元素不是通过索引来访问的,因此无法像数组或列表那样通过索引来直接访问元素。
(四) SortedSet<T>
SortedSet<T> 是 C# 中的一种有序集合,它根据元素的顺序自动对集合进行排序。
(五) 优点:
有序性:SortedSet<T> 中的元素是按照元素的顺序自动进行排序的。这使得查找操作更加高效,并且能够保持元素的有序性。
唯一性:SortedSet<T> 中的元素是唯一的,即集合中不允许包含重复的元素。这使得 SortedSet<T> 很适合于需要存储一组唯一元素的场景。
快速的查找操作:SortedSet<T> 提供了快速的查找操作。由于元素是按顺序存储的,因此平均时间复杂度为 O(log n),其中 n 是 SortedSet<T> 中的元素数量。
快速的插入和删除操作:SortedSet<T> 提供了快速的插入和删除操作。由于元素是按顺序存储的,因此平均时间复杂度为 O(log n)。
动态大小:SortedSet<T> 的大小可以根据需要动态增长或缩减,不需要事先指定容量。这使得 SortedSet<T> 在存储动态数据集时非常灵活。
(六) 缺点:
内存开销: 与其他集合类型相比,SortedSet<T> 可能会占用更多的内存空间。这是因为需要存储额外的索引信息来维护元素的有序性。
无法包含重复元素:SortedSet<T> 中的元素是唯一的,即集合中不允许包含重复的元素。如果尝试向 SortedSet<T> 中添加重复的元素,则添加操作会被忽略。
不支持索引访问:SortedSet<T> 中的元素不是通过索引来访问的,因此无法像数组或列表那样通过索引来直接访问元素。
不支持修改操作:SortedSet<T> 中的元素是不可修改的。如果需要修改元素,需要先从集合中删除该元素,然后再添加修改后的元素。
九、可观察集合
在 C# 中,可观察集合是一种特殊类型的集合,它实现了 .NET Framework 中的 INotifyCollectionChanged 接口,用于在集合发生更改时通知绑定到该集合的界面元素。可观察集合通常用于 WPF (Windows Presentation Foundation)、UWP (Universal Windows Platform) 和 Xamarin 应用程序中,以便在用户界面中动态地更新数据。
(一) 主要接口:
INotifyCollectionChanged: 这是 .NET Framework 中定义的接口,它定义了一个 CollectionChanged 事件,用于在集合发生更改时通知订阅者。通常,可观察集合会实现这个接口以便能够提供更改通知。
(二) 主要类:
ObservableCollection<T>: 这是 .NET Framework 中提供的一个具体的可观察集合类。它实现了 INotifyCollectionChanged 接口,并提供了用于向集合添加、移除和清除元素的方法。当集合中的元素发生更改时,ObservableCollection<T> 会触发 CollectionChanged 事件,以便通知绑定到该集合的界面元素更新显示。
(三) 优点:
数据绑定: 可观察集合通常用于在用户界面中绑定到数据源,以便在数据源发生更改时自动更新用户界面。这种机制使得在 UI 中动态显示数据变得非常简单,不需要手动管理数据与 UI 的同步。
简化开发: 使用可观察集合可以简化开发过程,因为不需要手动处理集合更改时的更新逻辑。只需订阅 CollectionChanged 事件,即可在集合发生更改时执行相应的操作。
适用于 MVVM 模式: 在 MVVM (Model-View-ViewModel) 架构中,可观察集合通常用作视图模型 (ViewModel) 中的数据源,以便在视图 (View) 中动态显示数据。它与绑定机制相结合,使得在 MVVM 模式中管理数据变得更加方便。
支持多种集合操作: 可观察集合支持多种集合操作,包括添加、移除、清除等。这使得在使用可观察集合时能够方便地对集合进行操作,并及时通知 UI 更新。
(四) 缺点:
性能开销: 可观察集合需要在每次集合更改时触发 CollectionChanged 事件,这可能会导致一定的性能开销。特别是在处理大量数据或频繁进行集合操作时,可能会影响性能。
内存占用: 可观察集合通常会占用一定的内存空间,因为它需要额外的数据结构来管理集合更改通知。对于大型数据集,可能会增加额外的内存开销。
不适用于所有场景: 尽管可观察集合在许多情况下非常有用,但并不是所有场景都适合使用。在一些对性能要求较高的场景或数据量较大的场景中,可能会考虑使用其他数据结构来代替可观察集合。
数据一致性: 可观察集合只能通知界面元素有关集合更改的情况,但无法保证数据的一致性。在多线程环境中对可观察集合进行操作时,可能会出现数据不一致的情况,需要开发人员额外注意数据同步的问题。
十、位数组
在 C# 中,位数组(BitArray)是一种特殊的数据结构,用于表示位序列的集合。它提供了一种有效地存储和操作位数据的方法。
(一) 优点:
节省空间:BitArray 使用位压缩存储数据,因此在存储大量布尔值时非常高效。相比于使用 bool[] 数组,BitArray 可以节省内存空间。
高效的位操作:BitArray 提供了按位操作的方法,如按位与、按位或、按位异或等。这对于处理位级数据非常有用,例如编码、解码、图像处理等。
初始化灵活:您可以使用不同的构造函数来初始化 BitArray,包括使用字节数组、布尔数组或整数数组。这使得初始化过程非常灵活。
(二) 缺点:
不可变性:一旦创建了 BitArray,其长度不可更改。如果需要更改长度,您需要创建一个新的 BitArray 并将数据复制到其中。
不支持泛型:BitArray 不是泛型类,因此无法存储除布尔值之外的其他类型。如果需要存储其他类型的数据,您需要使用其他集合类型。
性能问题:尽管 BitArray 在位操作方面非常高效,但在某些情况下,它可能不如直接使用原始数据类型(如 int 或 long)来处理位操作。
十一、不可变集合
在C#中,不可变集合是指一旦创建就无法修改的集合。这意味着无法向不可变集合添加、删除或修改元素。不可变集合在多线程环境下通常很有用,因为它们不会发生变化,因此不需要同步控制。
.NET框架提供了一些不可变集合类型,其中包括:
System.Collections.Immutable 命名空间中的不可变集合类型,如 ImmutableArray<T>,ImmutableList<T>,ImmutableDictionary<TKey, TValue> 等。这些类型提供了一组不可变的数组、列表和字典,它们支持一组操作来创建新的不可变实例,而不是在现有实例上进行更改。
例如,使用 ImmutableList<T> 创建一个不可变列表:
using System.Collections.Immutable;
ImmutableList<int> immutableList = ImmutableList.Create<int>(1, 2, 3);
然后,可以通过一系列不可变操作来创建新的不可变列表,而不是修改现有列表:
ImmutableList<int> newImmutableList = immutableList.Add(4).Add(5);
.NET Core 2.0 和 .NET Standard 2.0 引入了 System.Collections.Generic 命名空间中的 ReadOnlyCollection<T> 类,它提供了对现有集合的只读包装,从而使其看起来像不可变集合。但是,这些只读包装并非真正意义上的不可变集合,因为它们仍然是对原始集合的引用,如果原始集合发生更改,只读包装也会反映这些更改。
using System.Collections.Generic;
List<int> list = new List<int>() { 1, 2, 3 };
ReadOnlyCollection<int> readOnlyCollection = list.AsReadOnly();
尽管 ReadOnlyCollection<T> 本身是只读的,但是如果原始集合 list 发生更改,readOnlyCollection 也会受到影响。
以上是在C#中使用不可变集合的一些方法,它们可以帮助编写更安全、更易于维护的代码,特别是在多线程环境下。
(一) ReadOnlyCollection<T> VS ImmutableList<T>
ReadOnlyCollection<T> 和 ImmutableList<T> 都提供了一种方式来创建只读的集合,但它们之间存在一些关键区别:
可变性:
ReadOnlyCollection<T> 是对现有可变集合的只读包装。这意味着,虽然 ReadOnlyCollection<T> 本身是只读的,但它仍然引用原始集合,如果原始集合发生更改,ReadOnlyCollection<T> 也会反映这些更改。
ImmutableList<T> 是一种完全不可变的集合,一旦创建就无法修改。它提供了一组不可变操作来创建新的不可变实例,而不是在现有实例上进行更改。因此,ImmutableList<T> 是真正意义上的不可变集合。
由于 ReadOnlyCollection<T> 是对现有集合的包装,因此它的性能受到底层集合的影响。例如,如果原始集合是 List<T>,那么在执行一些操作时可能会引发性能开销,因为需要在可变集合上执行操作。
ImmutableList<T> 内部采用了一种持久化数据结构的实现方式,因此它的性能在大多数情况下都非常高效。虽然创建新的不可变实例可能会导致一些开销,但在大多数情况下,这些开销是可以接受的,并且不会导致性能问题。
ReadOnlyCollection<T> 只是提供了对现有集合的只读访问,不能进行任何修改操作。它本质上只是一个包装器,使得现有集合表现为只读。
ImmutableList<T> 则提供了一系列不可变操作,例如添加、移除、插入元素等,每个操作都会返回一个新的 ImmutableList<T> 实例,而不会修改原始实例。
因此,如果你需要一个真正不可变的集合,并且希望在多次操作后保持不可变性,那么应该使用 ImmutableList<T>。如果你只需要对现有集合进行只读访问,并且不需要进行修改操作,那么 ReadOnlyCollection<T> 可能更适合你的需求。
(二) 不可变集合应用场景
其中一些常见的应用场景包括:
函数式编程:不可变集合是函数式编程中的基本概念之一。函数式编程强调使用纯函数,即函数的输出仅依赖于输入,不会影响外部状态。不可变集合与纯函数式编程风格非常契合,因为它们确保数据不可变性,避免了副作用和共享状态,从而使得代码更易于理解和调试。
多线程编程:在并发和多线程编程中,共享状态可能会导致竞态条件和线程安全问题。使用不可变集合可以避免这些问题,因为数据不可变性意味着无需同步控制,多个线程可以安全地访问和操作不可变数据结构。
缓存:在缓存方面,不可变集合可以确保缓存数据不会被意外修改。例如,在缓存常用数据的场景中,如果使用不可变集合来存储缓存数据,就不必担心在其他地方对数据进行修改,从而确保缓存数据的一致性。
数据传输:在跨进程或跨网络传输数据时,不可变集合可以确保数据的完整性和一致性。由于不可变集合是只读的且不可修改的,因此发送方可以放心地将数据发送给接收方,而无需担心数据被篡改或修改。
历史记录:在需要记录数据历史状态的应用中,不可变集合可以很好地满足需求。通过保存每个操作的不可变副本,可以轻松地回溯到任意时间点的数据状态,而无需担心数据被覆盖或修改。
十二、并发集合
在C#中,为了支持并发编程,提供了几种并发集合,这些集合能够安全地在多个线程中进行读写操作,而不需要手动实现同步控制。以下是一些常见的并发集合:
ConcurrentDictionary<TKey, TValue>: ConcurrentDictionary<TKey, TValue> 是一个线程安全的字典,可以在多个线程中并发地进行读写操作而不需要显式的锁定。它提供了一系列的原子性操作,如 AddOrUpdate,GetOrAdd 等。
using System.Collections.Concurrent;
ConcurrentDictionary<string, int> concurrentDictionary = new ConcurrentDictionary<string, int>();
concurrentDictionary.TryAdd("key", 1);
int value;
bool exists = concurrentDictionary.TryGetValue("key", out value);
ConcurrentQueue<T> 和 ConcurrentStack<T>: ConcurrentQueue<T> 和 ConcurrentStack<T> 分别表示线程安全的队列和栈,它们允许多个线程并发地进行入队和出队(或入栈和出栈)操作,而不需要显式的同步控制。
using System.Collections.Concurrent;
ConcurrentQueue<int> concurrentQueue = new ConcurrentQueue<int>();
concurrentQueue.Enqueue(1);
bool success = concurrentQueue.TryDequeue(out int result);
ConcurrentBag<T>: ConcurrentBag<T> 表示一个无序的线程安全的集合,它允许多个线程并发地向其中添加和移除元素。
using System.Collections.Concurrent;
ConcurrentBag<int> concurrentBag = new ConcurrentBag<int>();
concurrentBag.Add(1);
bool success = concurrentBag.TryTake(out int result);
这些并发集合在设计上旨在提供高性能的并发访问,使得在多线程环境中进行数据共享变得更加容易和安全。它们允许多个线程同时读取和修改集合,而不会出现数据损坏或不一致的情况。因此,在需要在多线程环境中进行数据共享和并发访问时,使用这些并发集合是一个不错的选择。
(一) 优点:
高性能:并发集合在高并发场景下表现出色。它们减少了锁的使用,从而提高了性能。
线程安全:这些集合可以保证多线程环境下的数据一致性,避免了竞态条件和死锁问题。
原子操作:并发集合提供了原子的检测和执行操作,例如 TryPop 和 TryAdd。
简化线程管理:C# 提供了方便的线程管理机制,例如 Task Parallel Library (TPL),使线程创建和管理更加简单。
(二) 缺点:
性能相对较低:在非并发场景下,传统集合的性能通常更高。
枚举时的注意事项:在枚举并发集合时,如果另一个线程更新了集合的内容,不会抛出异常,而是得到一个新旧内容混合的结果。
内存利用不如非并发集合高效:并发集合内部使用链表实现,因此其内存利用不如非并发的 Stack 和 Queue 高效。
不适用于生产者/消费者队列:ConcurrentBag 不适用于实现生产者/消费者队列,因为元素的添加和移除操作是在不同的线程间执行的。