[.NET大牛之路 020] C# 高级:常见集合类型

数组无疑是我们用的最多也是最简单的数据结构。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 命名空间中的一些主要的非泛型集合类型。下面列举了这个命名空间中一些比较常用的集合类型:

  1. ArrayList:大小可动态变化的对象集合。
  2. BitArray:紧凑的位值数组,用于布尔运算。
  3. Hashtable:键值对的集合,这些键值对是根据键的哈希代码组织的。
  4. Queue:标准的先进先出(FIFO)的对象集合,即队列。
  5. SortedList:键值对的集合,这些键值对按键排序,并可按键和索引访问。
  6. Stack:后进先出(LIFO)的对象集合,即栈。

以上所有集合类型都继承了 ICollection IEnumerable ICloneable 接口,其中 Hashtable SortedList 是键值对集合,还继承了 IDictionary 接口。这些接口顾名思义,各自代表了相应的功能和特点,具体大家可以通过 VS 查看各个接口的成员方法了解。

在 .NET 早期版本这些集合用的比较多,.NET 支持泛型之后,非泛型集合用的情况就比较少了。主要因为相对泛型集合,非泛型集合存在两个缺点。

第一个缺点是,非泛型集合在存入与读取时会发生装箱与拆箱操作,所以性能不如泛型集合好。第二个缺点是,非泛型集合都是针对 Object 类型的操作,有类型安全问题。

02 泛型集合

泛型集合类型的引入就是为了解决非泛型集合存在的问题,毫无疑问,两者的拥有的类型大多是相似的,泛型集合类型相当于是对非泛型集合类型的扩充。

另外,泛型集合继承的也是对应的泛型接口。你可以发现许多泛型接口都扩展了它们的非泛型接口。例如,IEnumerable<T> 扩展了 IEnumerable

所有的泛型集合类型都定义在 System.Collections.Generic 命名空间中,下面列举了这个命名空间中一些比较常用的泛型集合类型:

  1. List<T>:可变大小的列表。
  2. SortedList<TKey, TValue>:键值对列表,按键排序。
  3. HashSet<T>:无重复对象的集合。
  4. SortedSet<T>:无重复且有序的对象集合。
  5. Dictionary<TKey, TValue>:无重复键值对集合。
  6. SortedDictionary<TKey, TValue>:按键排序无重复键值对集合。
  7. Queue<T>:队列,先进先出。
  8. 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,经授权后发布,本文观点不代表Chaoqiang's Blog立场,转载请联系原作者。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注

近期个人博客正在迁移中,原博客请移步此处,抱歉!