第 36 节 · 第四部分 进阶特性

集合(Collection)

集合是能变长的容器,六种各有取法。

时长 7:17授课计划第 14 周

学完这一节,你能

讲义

跟着视频的顺序写。每行代码都有注释,代码和运行结果都是实际运行得到的。

1集合是什么

数组的长度一开始就定死了;集合是 C# 准备好的一批专门存数据、找数据的类型,可以按下标取、按键取,还能随时添加、删除元素。

背后是几种常见的数据结构:栈后进先出,队列先进先出,列表是能自动变长的有序数据,哈希表存一对对键和值,按键查得很快。

2相同的用法

这些集合大多实现了相同的接口,所以都能添加、删除,都能用 foreach 遍历,也都有 Count 表示装了几个元素。下面六种集合各新建一个,打印各自的 Count。本节例子都是顶级语句写法(不写类和 Main,代码从第一行开始执行):

C#
1using System.Collections;// 引入 System.Collections 命名空间,六种集合都在这里
2
3ICollection[] boxes = {// ICollection 是它们共同实现的接口,这里建一个接口数组
4 new ArrayList(), new Hashtable(), new SortedList(),// 新建动态数组、哈希表、排序列表
5 new Stack(), new Queue(), new BitArray(8) };// 新建栈、队列、8 位的位数组;数组结束
6foreach (ICollection c in boxes)// 逐个取出每个集合
7 Console.WriteLine(c.GetType().Name + ":" + c.Count);// 打印类名和 Count(元素个数)
运行结果
ArrayList:0
Hashtable:0
SortedList:0
Stack:0
Queue:0
BitArray:8

前五个都是 0,BitArray 却是 8:位数组一新建就有 8 位,每一位默认是 false。

3元素按 object 存放

早期的集合把元素都按 object 类型存。所有类型最终都继承自 object,所以 ArrayList 里整数、字符串都能放;可取出来只是一个 object,直接交给 int 变量会报错:

C# · 第 3-6 行
3ArrayList al = new ArrayList();// 新建动态数组 al(ArrayList 下面细讲)
4al.Add(45);// 放进整数 45
5al.Add("abc");// 也能放进字符串 "abc"
6int first = al[0];// 取出的是 object,直接赋给 int
编译报错
Program.cs(6,13): error CS0266: 无法将类型“object”隐式转换为“int”。存在一个显式转换(是否缺少强制转换?)

得先强制转换,告诉编译器这其实是个 int:

C# · 第 6-7 行
6int first = (int)al[0];// (int) 是强制转换:把 object 转回 int
7Console.WriteLine(first);// 输出 45

所以微软文档建议新代码改用泛型的 List<T> 和 Dictionary<TKey,TValue>,取出来直接就是 int,不用强转(泛型下一章细讲):

C#
1List<int> nums = new List<int>();// List<int>:只装 int 的列表
2nums.Add(45);// 放进 45
3int first = nums[0];// 取出来就是 int,不用强转
4Console.WriteLine(first);// 输出 45
5
6Dictionary<string, int> ages = new Dictionary<string, int>();// 字典:键是 string,值是 int
7ages.Add("Amy", 20);// 存一对:键 "Amy",值 20
8Console.WriteLine(ages["Amy"]);// 按键 "Amy" 取值,输出 20
运行结果
45
20

4命名空间要写对

这六种集合都在 System.Collections 命名空间里,末尾有个 s,少了就报错:

C# · 第 1 行
1using System.Collection;// 少写了末尾的 s
编译报错
Program.cs(1,14): error CS0234: 命名空间“System”中不存在类型或命名空间名“Collection”(是否缺少程序集引用?)

5动态数组 ArrayList

像数组一样能按下标访问每一项,不同的是会自动调整大小。

C# · 第 3-11 行
3ArrayList al = new ArrayList();// 新建一个空的动态数组
4al.Add(45);// Add 加到末尾:[45]
5al.Add(78);// [45, 78]
6al.Add(33);// [45, 78, 33]
7al.Insert(1, 10);// Insert 在下标 1 插入 10,后面的往后挪:[45, 10, 78, 33]
8Console.WriteLine("个数:" + al.Count);// Count 是 4
9foreach (object x in al)// 按顺序逐个取出,每一项都是 object
10 Console.Write(x + " ");// 打印 45 10 78 33
11Console.WriteLine();// 换行
C# · 第 13-14 行
13al.Remove(78);// Remove 删掉 78,后面的往前补:[45, 10, 33]
14al.Sort();// Sort 按大小排序:[10, 33, 45]
运行结果
个数:4
45 10 78 33 
10 33 45

“有序”指保持放进去的先后顺序,不会自动排大小;要排大小得调用 Sort。

6哈希表 Hashtable

每一项都是一对键和值。存的时候先根据键算出哈希码,决定放进哪一格;取的时候用键再算一次,直接找到那一格。

C# · 第 3-10 行
3Hashtable ht = new Hashtable();// 新建哈希表
4ht.Add("Tom", 18);// 存一对:键 "Tom",值 18
5ht.Add("Amy", 20);// 键 "Amy",值 20
6ht.Add("Ben", 19);// 键 "Ben",值 19
7Console.WriteLine(ht["Amy"]);// 用键取值:输出 20
8Console.WriteLine(ht.ContainsKey("Bob"));// ContainsKey 问有没有这个键:没有 Bob,输出 False
9foreach (DictionaryEntry e in ht)// DictionaryEntry 表示一对键值
10 Console.WriteLine(e.Key + ":" + e.Value);// 打印键和值,顺序不一定
运行结果
20
False
Ben:19
Amy:20
Tom:18

同一个程序再运行一次,遍历顺序又变了。哈希表只管按键找得快,不保证顺序:

再运行一次的结果
20
False
Tom:18
Ben:19
Amy:20

类名是 Hashtable,t 小写;写成大写 T 的 HashTable 就报错(教程开头这样写是笔误):

C# · 第 3 行
3HashTable ht = new Hashtable();// 类型名写成大写 T
编译报错
Program.cs(3,1): error CS0246: 未能找到类型或命名空间名“HashTable”(是否缺少 using 指令或程序集引用?)

7排序列表 SortedList

既能按键取,也能按下标取。它里面是两个数组,一个放键、一个放值,每放进一项都按键的顺序插到该在的位置。

C# · 第 3-10 行
3SortedList sl = new SortedList();// 新建排序列表
4sl.Add("Tom", 18);// 放进 Tom:[Tom]
5sl.Add("Amy", 20);// Amy 按字母排到最前:[Amy, Tom]
6sl.Add("Ben", 19);// Ben 插到中间:[Amy, Ben, Tom]
7Console.WriteLine(sl["Ben"]);// 按键取:输出 19
8Console.WriteLine(sl.GetKey(0) + ":" + sl.GetByIndex(0));// 按下标 0 取键和值:Amy:20
9foreach (DictionaryEntry e in sl)// 遍历每一对键值
10 Console.WriteLine(e.Key + ":" + e.Value);// 按键排好的顺序打印
运行结果
19
Amy:20
Amy:20
Ben:19
Tom:18

排好序的代价是放进去时要挪位置,通常比哈希表慢。

8堆栈 Stack

这里的堆栈就是前面说的栈,和内存里的堆没有关系。它后进先出,像一摞盘子,只能从最上面放、从最上面拿。

C# · 第 3-10 行
3Stack st = new Stack();// 新建栈
4st.Push('A');// Push 推入:A
5st.Push('B');// 底 A、B 顶
6st.Push('C');// 底 A、B、C 顶
7Console.WriteLine(st.Peek());// Peek 只看最上面的,不拿走:C
8Console.WriteLine(st.Pop());// Pop 弹出最上面的:C
9Console.WriteLine(st.Pop());// 再弹出:B
10Console.WriteLine("还剩 " + st.Count + " 个");// 只剩 A,Count 是 1
运行结果
C
C
B
还剩 1 个

9队列 Queue

先进先出,像排队打饭,先来的先走。

C# · 第 3-10 行
3Queue q = new Queue();// 新建队列
4q.Enqueue('A');// Enqueue 入队:队头 A
5q.Enqueue('B');// 队头 A、B 队尾
6q.Enqueue('C');// 队头 A、B、C 队尾
7Console.WriteLine(q.Peek());// Peek 只看队头,不拿走:A
8Console.WriteLine(q.Dequeue());// Dequeue 从队头出队:A
9Console.WriteLine(q.Dequeue());// 再出队:B
10Console.WriteLine("还剩 " + q.Count + " 个");// 只剩 C,Count 是 1
运行结果
A
A
B
还剩 1 个

栈和队列放进去的顺序一样,拿出来的顺序正好相反。

10点阵列 BitArray

也叫位数组,每一格只存一位:1 或 0,在 C# 里就是 true 或 false,下标从 0 开始。

C# · 第 3-10 行
3BitArray ba = new BitArray(8);// 新建 8 位的位数组,每位默认 false
4ba[0] = true;// 第 0 位设成 true
5ba[3] = true;// 第 3 位设成 true
6foreach (bool b in ba)// 逐位取出,每位是 bool
7 Console.Write(b ? 1 : 0);// ? : 是条件运算符:true 打印 1,false 打印 0
8Console.WriteLine();// 换行
9ba.Length = 10;// 把位数改成 10
10Console.WriteLine(ba.Length);// 输出 10
运行结果
10010000
10

教程说事先不知道要几位时用它:位数可以改,把 Length 设成 10 就变成十位。

11小结

  • 动态数组 ArrayList:按下标取,能自动变长。
  • 哈希表 Hashtable:按键取,不保证顺序。
  • 排序列表 SortedList:按键、按下标都能取,总是按键排好序。
  • 栈 Stack 后进先出,队列 Queue 先进先出。
  • 位数组 BitArray:每格一位,存 true 或 false。
  • 写新代码时,优先用 List、Dictionary 这些泛型集合。

下一节:ArrayList 取出来是 object,每次都要强转。能不能让集合一开始就说好只装 int?

检查题

5 道,选完马上告诉你对不对。

  1. 1al.Add(45); al.Add(78); al.Add(33); al.Insert(1, 10); 后遍历输出?

  2. 2Stack 依次 Push A、B、C,再 Peek、Pop、Pop,打印的三个字母?

  3. 3Hashtable 依次加 Tom、Amy、Ben 后 foreach,顺序一定是?

  4. 4SortedList 加 Tom、Amy、Ben 后 GetKey(0)?

  5. 5ArrayList al…; int first = al[0]; 结果?