数组无疑是我们用的最多也是最简单的数据结构。C# 数组允许你定义一组具有固定长度的相同类型的元素(包括 Object
的数组,它代表任何类型)。考虑以下代码,它创建了一个字符串数组,并尝试追加内容:
// 创建数组
string[] strArray = { "First", "Second", "Third" };
// 尝试在末尾添加一个元素将报错
strArray[3] = "new item";
注:实际上,可以使用通用的 Resize()<T>
方法来改变数组的大小。然而,这将导致数据被复制到一个新的数组对象中,并且可能是低效的。
数组虽然简单,但正如上面演示的,它有局限性。.NET Core 基础类库中有很多集合类。与简单的 C# 数组不同,集合类支持你在插入或删除项目时动态地调整自己的大小。此外,大部分集合类都支持泛型,并且针对其特性进行了高度优化。在 C# 中,所有的集合都在这两个命名空间中:
- 非泛型集合在
System.Collections
命名空间中; - 泛型集合在
System.Collections.Generic
命名空间中。
大部分非泛型集合是对 Object
类型的操作,因此是松散类型的容器。相比之下,泛型集合是类型安全的,你必须在创建时指定它们所包含元素的类型。
01 非泛型集合
我们先来看看 System.Collections
命名空间中的一些主要的非泛型集合类型。下面列举了这个命名空间中一些比较常用的集合类型:
ArrayList
:大小可动态变化的对象集合。BitArray
:紧凑的位值数组,用于布尔运算。Hashtable
:键值对的集合,这些键值对是根据键的哈希代码组织的。Queue
:标准的先进先出(FIFO)的对象集合,即队列。SortedList
:键值对的集合,这些键值对按键排序,并可按键和索引访问。Stack
:后进先出(LIFO)的对象集合,即栈。
以上所有集合类型都继承了 ICollection
、 IEnumerable
和 ICloneable
接口,其中 Hashtable
和 SortedList
是键值对集合,还继承了 IDictionary
接口。这些接口顾名思义,各自代表了相应的功能和特点,具体大家可以通过 VS 查看各个接口的成员方法了解。
在 .NET 早期版本这些集合用的比较多,.NET 支持泛型之后,非泛型集合用的情况就比较少了。主要因为相对泛型集合,非泛型集合存在两个缺点。
第一个缺点是,非泛型集合在存入与读取时会发生装箱与拆箱操作,所以性能不如泛型集合好。第二个缺点是,非泛型集合都是针对 Object 类型的操作,有类型安全问题。
02 泛型集合
泛型集合类型的引入就是为了解决非泛型集合存在的问题,毫无疑问,两者的拥有的类型大多是相似的,泛型集合类型相当于是对非泛型集合类型的扩充。
另外,泛型集合继承的也是对应的泛型接口。你可以发现许多泛型接口都扩展了它们的非泛型接口。例如,IEnumerable<T>
扩展了 IEnumerable
。
所有的泛型集合类型都定义在 System.Collections.Generic
命名空间中,下面列举了这个命名空间中一些比较常用的泛型集合类型:
List<T>
:可变大小的列表。SortedList<TKey, TValue>
:键值对列表,按键排序。HashSet<T>
:无重复对象的集合。SortedSet<T>
:无重复且有序的对象集合。Dictionary<TKey, TValue>
:无重复键值对集合。SortedDictionary<TKey, TValue>
:按键排序无重复键值对集合。Queue<T>
:队列,先进先出。Stack<T>
:栈,后进先出。
除了上面这些, System.Collections.Generic
命名空间还定义了许多其它集合类型。例如, LinkedList<T>
类型表示双链表结构的集合,针对频繁在邻接节点做插入和删除的操作比普通的 List<T>
类型要高效。关于更多的集合类型及其细节,请查阅 .NET 官方文档。
下面选几个典型的集合类型简单过一下他们的用法吧。
List<T>
与 HashSet<T>
为了方便演示,我们先创建一个作为集合元素的Person
类型:
public class Person
{
public int Age { get; set; }
public string Name { get; set; }
public Person() { }
public Person(string name, int age)
{
Age = age;
Name = name;
}
public override string ToString()
{
return $"Name: {Name}, Age: {Age}";
}
}
List<T>
类是 System.Collections.Generic
命名空间中最常使用的类型,因为它可以动态调整内容大小,可以方便地进行新增、插入和移除操作。
HashSet<T>
和 List<T>
差不多,只是它没有插入操作,且内容元素是不重复的,添加相同元素会被自动去重。
下面利用 List<T>
和 HashSet<T>
来操作一组 Person 对象:
static void Main(string[] args)
{
Person person1 = new() { Name = "Name1", Age = 37 };
Person person2 = new() { Name = "Name2", Age = 35 };
// 初始化一个 List<Person>
List<Person> list = new() { person1, person2, };
// 初始化一个 HashSet<Person>
HashSet<Person> set = new() { person1, person2, };
// 打印数量个数
Console.WriteLine("Items in List<Person>: {0}", list.Count);
Console.WriteLine("Items in HashSet<Person>: {0}", set.Count);
Person person3 = new() { Name = "Name3", Age = 29 };
Console.WriteLine("\nInserting new person twice.");
// 重复添加元素到 List<Person>
list.Add(person3);
list.Add(person3);
// 重复添加元素到 HashSet<Person>
set.Add(person3);
set.Add(person3);
// 再次打印数量个数
Console.WriteLine("Items in List<Person>: {0}", list.Count);
Console.WriteLine("Items in HashSet<Person>: {0}", set.Count);
Console.ReadLine();
}
打印结果:
Items in List<Person>: 2
Items in HashSet<Person>: 2
Inserting new person twice.
Items in List<Person>: 4
Items in HashSet<Person>: 3
可以看到,向两个集合类型重复添加同一元素时, HashSet<T>
自动去重了。这两种集合类型定义了许多有用的成员,就不一一演示了,具体用到时大家再查阅文档了解更多信息吧。
Stack<T>
和 Queue<T>
Stack<T>
类代表一个后进先出数据结构的集合,称为堆栈或栈。 Stack<T>
的两个主要操作是进栈和出栈,对应的方法是 Push()
和 Pop()
,用于将项目放入栈中或从栈中移除。另外, Peek()
用来查看栈顶元素。下面是一个简单的示例:
static void Main(string[] args)
{
Stack<Person> peopleStack = new();
// 相继入栈新元素
peopleStack.Push(new() { Name = "Name1", Age = 37 });
peopleStack.Push(new() { Name = "Name2", Age = 29 });
peopleStack.Push(new() { Name = "Name3", Age = 25 });
// 查看栈顶元素
Console.WriteLine("First person is: {0}", peopleStack.Peek());
// 将栈顶元素出栈
Console.WriteLine("Popped off {0}", peopleStack.Pop());
// 再查看栈顶元素
Console.WriteLine("First person is: {0}", peopleStack.Peek());
Console.ReadLine();
}
输出结果:
First person is: Name: Name3, Age: 25
Popped off Name: Name3, Age: 25
First person is: Name: Name2, Age: 29
Queue<T>
和 Stack<T>
是相对的。 Queue<T>
是先进先出的数据结构集合,称为队列。这个很好理解,和人们平时生活中的排除是一个意思。 Queue<T>
的两个主要操作是入队和出队,对应的方法是 Enqueue()
和 Dequeue()
。同样用示例简单演示一下,假设有一群人正在排除买咖啡:
static void Main(string[] args)
{
Queue<Person> peopleQueue = new();
peopleQueue.Enqueue(new() { Name = "Name1", Age = 37 });
peopleQueue.Enqueue(new() { Name = "Name2", Age = 29 });
peopleQueue.Enqueue(new() { Name = "Name3", Age = 25 });
Console.WriteLine("{0} is first in line.", peopleQueue.Peek().Name);
GetCoffee(peopleQueue.Dequeue());
Console.WriteLine("{0} is first in line.", peopleQueue.Peek().Name);
Console.ReadLine();
}
static void GetCoffee(Person p)
{
Console.WriteLine("{0} got coffee!", p.Name);
}
输出结果:
Name1 is first in line.
Name1 got coffee!
Name2 is first in line.
需要注意的是,当 Queue<T>
或 Stack<T>
已经没有元素时, Pop()
或 Dequeue()
操作都会引发异常。
3小结
每一个集合类型都有其适用的场景,它们之间功能上的特点也很好区别,都是由集合本身的数据结构决定的。C# 提供了丰富的集合类型,除了本文提及的常用集合类型,还有很多存在于其它命名空间中的集合类型,大多是对这些常用集合的扩展。比如在 System.Collections.Concurrent
命名空间中专门定义 ConcurrentQueue<T>
、BlockingCollection<T>
等集合类型,用于一些和并行处理相关的场景。
本文来自http://cnblogs.com/willick,经授权后发布,本文观点不代表个人技术分享立场,转载请联系原作者。