英文原文:https://www.dotnetnakama.com/blog/one-step-beyond-by-using-dotnet-collections-to-its-fullest/
介绍
在 .NET 中,我们可以创建和管理可以根据我们的需要动态增长和收缩的集合(即相关对象组)。 或者,我们可以使用对象数组。 但是,当我们需要固定数量的强类型对象时,可以使用数组。
正如我们所理解的,集合在处理对象组时可以提供很大的灵活性。 因此,让我们从头开始。 .NET 提供了多种集合。 每个集合都是一个我们应该实例化的类,然后我们可以使用提供的方法管理(例如,添加、删除、过滤等)它的项目。
例如,我们可能需要相同数据类型(例如字符串)的项目或具有键/值对的集合(例如,字符串值的整数键)。 在这种情况下,我们可以使用 System.Collections.Generic 命名空间(例如 List< string >, Dictionary<int, string>)。
在本文中,我们将了解以下集合命名空间,它们最常用的类,何时可以使用它们,进行一些比较并了解 yield 关键字。
- System.Collections:包括将元素存储为 Object 类型的 ArrayList、Hashtable、Queue 和 Stack 的遗留类型。
- System.Collections.Generic:为特定数据类型创建泛型 (< T >) 集合。
- System.Collections.Concurrent:提供线程安全的集合类,从多个线程访问集合项
- System.Collections.Immutable:提供不可变的集合接口和类(不能改变的线程安全集合)。
.NET Collections
System.Collections
System.Collections 命名空间包含定义各种对象集合的接口和类。 此命名空间的定义类型是遗留的,因此不建议将其用于新开发。 下表显示了 System.Collections.Generic 命名空间中常用的类及其推荐的替代方案。
遗留类 | 描述 | 推荐类 |
---|---|---|
ArrayList | 一个对象数组,其大小根据需要动态增加(实现 IList 接口)。 | List |
Hashtable | 根据键的哈希码组织的键/值对的集合。 哈希表比字典慢,因为它需要装箱和拆箱。 | Dictionary |
Queue | 对象的先进先出 (FIFO) 集合。 | Queue |
Stack | 对象的后进先出 (LIFO) 集合。 | Stack |
System.Collections.Generic
System.Collections.Generic 命名空间提供接口和通用集合类来创建具有相同数据类型的集合(通过强制强类型)。 因此,当创建一个泛型集合类的实例时,例如 List< T >、Dictionary<TKey, TValue> 等,我们应该将 T 参数替换为我们的对象的类型。
例如,我们可以保留字符串值列表 (List< string >)、自定义用户对象列表 (List< User >)、具有字符串值的整数键字典 (Dictionary<int, string>) 等。
每个通用集合类都有其目的和用途。 例如,在 Dictionary<TKey, TValue> 中,我们可以添加与唯一键配对的项目(对象或值类型),并使用键快速检索项目。 在下一节中,我们可以看到一些常用的泛型集合类。
Dictionary
字典为值项提供成对键的集合。
- 每个键在集合中必须是唯一的(没有重复的键)。
- 它被实现为一个哈希表。 因此,使用键检索值非常快(接近 O(1))。
- 键不能为空,但值可以。
- 出于枚举目的,每个项目都表示为 KeyValuePair 结构。
- 对于不可变字典类,请参阅 ImmutableDictionary。
- 对于只读词典,请参阅 ReadOnlyDictionary。
代码示例:
// Initialize the Dictionary
Dictionary<int, string> myDictionary = new Dictionary<int, string>();
// Add items into the Dictionary
myDictionary.Add(2, "A string value-2");
myDictionary.Add(1, "A string value-1");
myDictionary.Add(3, "A string value-3");
// Try to add an item by first checking if it exists.
if(!myDictionary.TryAdd(3, "A new value"))
{
Console.WriteLine("The provided key (3) already exists in the dictionary.");
}
// Check if a key already exists.
if (!myDictionary.ContainsKey(4))
{
myDictionary.Add(4, "A string value-4");
}
// Modify the value of the key with value: 1 (not the index).
myDictionary[1] = "A string value-1: Modified!";
// Get and show the value of a specific key.
Console.WriteLine($"The value of key:3 is: {myDictionary[3]}");
// Enumerate the items in the dictionary.
foreach (KeyValuePair<int, string> keyValueItem in myDictionary)
{
Console.WriteLine($"Key:{keyValueItem.Key}. Value:{keyValueItem.Value}");
}
// Output:
// The provided key (3) already exists in the dictionary.
// The value of key:3 is: A string value-3
// Key:2. Value:A string value-2
// Key:1. Value:A string value-1: Modified!
// Key:3. Value:A string value-3
// Key:4. Value:A string value-4
List
列表是可以通过其索引访问的对象的强类型列表(请参见下面的代码示例)。
- 我们可以使用 Add 或 AddRange 方法添加项目。
- 它不能保证被排序。
- 我们可以使用整数索引(从零开始)访问项目。
- 接受空值。
- 允许重复项目。
- 对于不可变列表类,请参阅 ImmutableList。
- 有关只读列表,请参阅 IReadOnlyCollection。
代码示例:
// Initialize the List (with some items)
List<string> aList = new List<string>() { "Item-1", "Item-2" };
// Add Items in the list
aList.Add("Item-3");
aList.Add("Item-3");
// Access an item by using its index
aList[3] = "Item-4";
// Check if an item already exists.
if (aList.Contains("Item-2"))
{
Console.WriteLine("The provided item already exists in the list.");
}
// Remove an item from the list
aList.Remove("Item-2");
// Enumerate the items in the list.
foreach (string listItem in aList)
{
Console.WriteLine($"List Item:{listItem}");
}
// Output:
// The provided item already exists in the list.
// List Item:Item-1
// List Item:Item-3
// List Item:Item-4
Queue 和 Stack
队列和堆栈可能是您在计算机科学课上学习计算机语言时最先学到的东西之一。 通常,队列和堆栈用于临时存储按特定顺序使用(访问)的信息。
提示:要在使用队列和堆栈时提高应用程序的性能,请根据您的情况设置较高的初始容量。 当需要增加容量时执行繁重的进程(例如,重新分配队列中的内部数组,堆栈 Push 方法成为 O(n) 操作)。
Queue
- 队列用于按照存储在集合中的相同顺序访问信息(图 1),即先进先出 (FIFO)。
- 为了轻松记住队列的概念,想象一下人们排队等候喝咖啡(即,一排人)。 排在第一位的将是第一个被送达的人。
- 使用 Enqueue 方法在 Queue 的末尾添加一个 T 项。
- 使用 Dequeue 方法从队列开头获取和删除最旧的 T 项。
- 使用 Peek 方法获取(查看)下一个要出队的 T 项目。
Stack
- 堆栈用于以与存储相反的顺序访问信息(图 2),即后进先出 (LIFO)。
- 为了轻松记住堆栈的概念,请想象一堆盒子(一个叠一个)。 如果您需要移动它们,您可能会在最上面找到一个(即您最后存储的)。
- 使用 Push 方法在 Stack< T > 的顶部添加一个 T 项。
- 使用 Pop 方法从 Stack< T > 的顶部获取和移除 T 项。
- 使用 Peek 方法获取(查看)将弹出的下一个 T 项。
PriorityQueue(优先队列)
在 .NET 6.0 中,在 System.Collections.Generic 命名空间中引入了 PriorityQueue 集合,我们可以在其中添加(即排队)具有值和优先级的新项目。 出队时,具有最低优先级值的项目将被删除。 .NET 文档指出,在优先级相同的情况下,不能保证先进先出语义。
System.Collections.Concurrent
System.Collections.Concurrent 命名空间提供了几个线程安全的集合类,当多个线程并发访问集合时,我们应该使用它们来代替 System.Collections.Generic 和 System.Collections 命名空间中的相应类型。 在下表中,我们可以看到一些经常使用的并发集合类。
并发类集合 | 描述 |
---|---|
ConcurrentDictionary | 提供成对的 TKey 到 TValue 项的线程安全集合。 |
ConcurrentQueue | 提供线程安全队列,即先进先出 (FIFO)。 |
ConcurrentStack | 提供线程安全的堆栈,即后进先出 (LIFO)。 |
System.Collections.Immutable
System.Collections.Immutable 命名空间提供了不可变的集合,可以向其消费者保证该集合永远不会改变。 此外,它们还提供隐式线程安全。 因此,我们不需要锁来访问集合。 在下表中,我们可以看到一些常用的不可变集合类。
不可变类集合 | 描述 |
---|---|
ImmutableDictionary | 提供成对的 TKey 到 TValue 项目的不可变集合。 |
ImmutableList | 提供一个不可变列表(由索引访问的强类型对象)。 |
ImmutableArray | 提供创建不可变数组的方法(创建后无法更改)。 |
超越一步
Yield 上下文关键字
正如 .NET 文档所述,yield 是语句中用于指示迭代器的上下文关键字(例如,在方法中使用时)。 通过使用 yield,我们可以定义一个迭代器(例如,一个 IEnumerable< T >),而无需创建一个临时集合(例如,一个 List< T >)来保存枚举器的状态。
在下面的代码示例中,我们可以看到在使用 yield return 时不需要临时集合。 好吧,示例函数可能很傻,但是你明白了 yield 关键字的要点😉。 如果我们想声明迭代结束,我们可以使用 yield break 语句。
public IEnumerable<int> GetNumbers(int from, int to)
{
List<int> numbers = new List<int>();
for (int i = from; i <= to; i++)
{
numbers.Add(i);
}
return numbers;
}
// By using the Yield keyword, we do not need a temporary collection.
public IEnumerable<int> GetNumbersUsingYield(int from, int to)
{
for (int i = from; i <= to; i++)
{
yield return i;
}
}
Immutable VS ReadOnly Collections
在前面的部分中,我们看到我们可以将我们的集合“转换”为 ReadOnly 或 Immutable。 根据它们的名称,我们可以假设在这两种情况下我们都无法对集合进行更改。 那么,它们有什么区别呢? 让我们从以下代码示例开始,了解如何将我们的 Dictionary 或 List 集合“转换”为 ReadOnly 或 Immutable。
// Create a normal dictionary
Dictionary<int, string> aDictionary = new Dictionary<int, string>();
aDictionary.Add(1, "A value");
aDictionary.Add(2, "Another value");
// Create an Immutable dictionary.
ImmutableDictionary<int,string> anImmutableDictionary = aDictionary.ToImmutableDictionary();
// Wrap the dictionary as ReadOnly.
ReadOnlyDictionary<int, string> aReadOnlyDictionary = new ReadOnlyDictionary<int, string>(aDictionary);
// Create a normal list
List<string> aList = new List<string>() { "Value1", "Value2" };
// Create an Immutable list from a normal list.
ImmutableList<string> anImmutableList = aList.ToImmutableList();
// Wrap a normal list as ReadOnly.
ReadOnlyCollection<string> aReadOnlyList = aList.AsReadOnly();
在 C# 中,只读集合只是实际集合(例如示例中的 aList)的包装器,它通过不提供相关方法来防止被修改。 但是,如果修改了实际集合,则 ReadOnlyCollection 也会更改。 此外,重要的是要注意只读集合不是线程安全的。
System.Collections.Immutable 命名空间包含定义不可变集合的接口和类,例如 ImmutableList< T >、ImmutableDictionary<TKey,TValue> 等。通过使用不可变集合,我们可以向我们的消费者保证集合永远不会改变。 此外,它们还提供隐式线程安全。 因此,我们不需要锁来访问集合。 重要的是要注意不可变集合提供修改方法(例如,添加),但它们不会进行任何修改并创建新的集合实例。
提示:不可变集合可以与记录(不可变类或结构)组合以提供完整、不可变的数据。
因此,正如我们所理解的,ReadOnly 和 Immutable 集合是完全不同的。
概括
.NET 提供了几个集合命名空间,在处理对象组时提供了极大的灵活性。 但是,System.Collections 命名空间被认为是遗留的,因此不建议用于新开发。 System.Collections.Generic 命名空间可以用作替代方法。
System.Collections.Generic 命名空间提供接口和通用集合类来创建具有相同数据类型的集合(通过强制强类型)。 它是最常用的集合命名空间,提供字典、列表、队列、堆栈和优先级队列等。
在处理并发请求(多线程)时,一个必不可少的(必须知道的)命名空间是 System.Collections.Concurrent 命名空间。 它提供了已知字典、队列和堆栈的并发(线程安全)版本。
ReadOnly 和 Immutable 集合有很大的不同,但是当我们需要无法更改的集合时,它们都提供了必要的功能。
.NET 提供了很棒的集合命名空间(工具),我们可以根据需要使用它们 😃。
如果您喜欢(或不喜欢)这篇文章,请不要犹豫,留下评论、问题、建议、投诉,或者在下面的部分打个招呼。 别当陌生人😉!