1集合是什么
数组的长度一开始就定死了;集合是 C# 准备好的一批专门存数据、找数据的类型,可以按下标取、按键取,还能随时添加、删除元素。
背后是几种常见的数据结构:栈后进先出,队列先进先出,列表是能自动变长的有序数据,哈希表存一对对键和值,按键查得很快。
2相同的用法
这些集合大多实现了相同的接口,所以都能添加、删除,都能用 foreach 遍历,也都有 Count 表示装了几个元素。下面六种集合各新建一个,打印各自的 Count。本节例子都是顶级语句写法(不写类和 Main,代码从第一行开始执行):
| 1 | using System.Collections; | // 引入 System.Collections 命名空间,六种集合都在这里 |
| 2 | | |
| 3 | ICollection[] boxes = { | // ICollection 是它们共同实现的接口,这里建一个接口数组 |
| 4 | new ArrayList(), new Hashtable(), new SortedList(), | // 新建动态数组、哈希表、排序列表 |
| 5 | new Stack(), new Queue(), new BitArray(8) }; | // 新建栈、队列、8 位的位数组;数组结束 |
| 6 | foreach (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 变量会报错:
| 3 | ArrayList al = new ArrayList(); | // 新建动态数组 al(ArrayList 下面细讲) |
| 4 | al.Add(45); | // 放进整数 45 |
| 5 | al.Add("abc"); | // 也能放进字符串 "abc" |
| 6 | int first = al[0]; | // 取出的是 object,直接赋给 int |
Program.cs(6,13): error CS0266: 无法将类型“object”隐式转换为“int”。存在一个显式转换(是否缺少强制转换?)
得先强制转换,告诉编译器这其实是个 int:
| 6 | int first = (int)al[0]; | // (int) 是强制转换:把 object 转回 int |
| 7 | Console.WriteLine(first); | // 输出 45 |
所以微软文档建议新代码改用泛型的 List<T> 和 Dictionary<TKey,TValue>,取出来直接就是 int,不用强转(泛型下一章细讲):
| 1 | List<int> nums = new List<int>(); | // List<int>:只装 int 的列表 |
| 2 | nums.Add(45); | // 放进 45 |
| 3 | int first = nums[0]; | // 取出来就是 int,不用强转 |
| 4 | Console.WriteLine(first); | // 输出 45 |
| 5 | | |
| 6 | Dictionary<string, int> ages = new Dictionary<string, int>(); | // 字典:键是 string,值是 int |
| 7 | ages.Add("Amy", 20); | // 存一对:键 "Amy",值 20 |
| 8 | Console.WriteLine(ages["Amy"]); | // 按键 "Amy" 取值,输出 20 |
45 20
4命名空间要写对
这六种集合都在 System. 命名空间里,末尾有个 s,少了就报错:
| 1 | using System.Collection; | // 少写了末尾的 s |
Program.cs(1,14): error CS0234: 命名空间“System”中不存在类型或命名空间名“Collection”(是否缺少程序集引用?)
5动态数组 ArrayList
像数组一样能按下标访问每一项,不同的是会自动调整大小。
| 3 | ArrayList al = new ArrayList(); | // 新建一个空的动态数组 |
| 4 | al.Add(45); | // Add 加到末尾:[45] |
| 5 | al.Add(78); | // [45, 78] |
| 6 | al.Add(33); | // [45, 78, 33] |
| 7 | al.Insert(1, 10); | // Insert 在下标 1 插入 10,后面的往后挪:[45, 10, 78, 33] |
| 8 | Console.WriteLine("个数:" + al.Count); | // Count 是 4 |
| 9 | foreach (object x in al) | // 按顺序逐个取出,每一项都是 object |
| 10 | Console.Write(x + " "); | // 打印 45 10 78 33 |
| 11 | Console.WriteLine(); | // 换行 |
| 13 | al.Remove(78); | // Remove 删掉 78,后面的往前补:[45, 10, 33] |
| 14 | al.Sort(); | // Sort 按大小排序:[10, 33, 45] |
个数:4 45 10 78 33 10 33 45
“有序”指保持放进去的先后顺序,不会自动排大小;要排大小得调用 Sort。
6哈希表 Hashtable
每一项都是一对键和值。存的时候先根据键算出哈希码,决定放进哪一格;取的时候用键再算一次,直接找到那一格。
| 3 | Hashtable ht = new Hashtable(); | // 新建哈希表 |
| 4 | ht.Add("Tom", 18); | // 存一对:键 "Tom",值 18 |
| 5 | ht.Add("Amy", 20); | // 键 "Amy",值 20 |
| 6 | ht.Add("Ben", 19); | // 键 "Ben",值 19 |
| 7 | Console.WriteLine(ht["Amy"]); | // 用键取值:输出 20 |
| 8 | Console.WriteLine(ht.ContainsKey("Bob")); | // ContainsKey 问有没有这个键:没有 Bob,输出 False |
| 9 | foreach (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 就报错(教程开头这样写是笔误):
| 3 | HashTable ht = new Hashtable(); | // 类型名写成大写 T |
Program.cs(3,1): error CS0246: 未能找到类型或命名空间名“HashTable”(是否缺少 using 指令或程序集引用?)
7排序列表 SortedList
既能按键取,也能按下标取。它里面是两个数组,一个放键、一个放值,每放进一项都按键的顺序插到该在的位置。
| 3 | SortedList sl = new SortedList(); | // 新建排序列表 |
| 4 | sl.Add("Tom", 18); | // 放进 Tom:[Tom] |
| 5 | sl.Add("Amy", 20); | // Amy 按字母排到最前:[Amy, Tom] |
| 6 | sl.Add("Ben", 19); | // Ben 插到中间:[Amy, Ben, Tom] |
| 7 | Console.WriteLine(sl["Ben"]); | // 按键取:输出 19 |
| 8 | Console.WriteLine(sl.GetKey(0) + ":" + sl.GetByIndex(0)); | // 按下标 0 取键和值:Amy:20 |
| 9 | foreach (DictionaryEntry e in sl) | // 遍历每一对键值 |
| 10 | Console.WriteLine(e.Key + ":" + e.Value); | // 按键排好的顺序打印 |
19 Amy:20 Amy:20 Ben:19 Tom:18
排好序的代价是放进去时要挪位置,通常比哈希表慢。
8堆栈 Stack
这里的堆栈就是前面说的栈,和内存里的堆没有关系。它后进先出,像一摞盘子,只能从最上面放、从最上面拿。
| 3 | Stack st = new Stack(); | // 新建栈 |
| 4 | st.Push('A'); | // Push 推入:A |
| 5 | st.Push('B'); | // 底 A、B 顶 |
| 6 | st.Push('C'); | // 底 A、B、C 顶 |
| 7 | Console.WriteLine(st.Peek()); | // Peek 只看最上面的,不拿走:C |
| 8 | Console.WriteLine(st.Pop()); | // Pop 弹出最上面的:C |
| 9 | Console.WriteLine(st.Pop()); | // 再弹出:B |
| 10 | Console.WriteLine("还剩 " + st.Count + " 个"); | // 只剩 A,Count 是 1 |
C C B 还剩 1 个
9队列 Queue
先进先出,像排队打饭,先来的先走。
| 3 | Queue q = new Queue(); | // 新建队列 |
| 4 | q.Enqueue('A'); | // Enqueue 入队:队头 A |
| 5 | q.Enqueue('B'); | // 队头 A、B 队尾 |
| 6 | q.Enqueue('C'); | // 队头 A、B、C 队尾 |
| 7 | Console.WriteLine(q.Peek()); | // Peek 只看队头,不拿走:A |
| 8 | Console.WriteLine(q.Dequeue()); | // Dequeue 从队头出队:A |
| 9 | Console.WriteLine(q.Dequeue()); | // 再出队:B |
| 10 | Console.WriteLine("还剩 " + q.Count + " 个"); | // 只剩 C,Count 是 1 |
A A B 还剩 1 个
栈和队列放进去的顺序一样,拿出来的顺序正好相反。
10点阵列 BitArray
也叫位数组,每一格只存一位:1 或 0,在 C# 里就是 true 或 false,下标从 0 开始。
| 3 | BitArray ba = new BitArray(8); | // 新建 8 位的位数组,每位默认 false |
| 4 | ba[0] = true; | // 第 0 位设成 true |
| 5 | ba[3] = true; | // 第 3 位设成 true |
| 6 | foreach (bool b in ba) | // 逐位取出,每位是 bool |
| 7 | Console.Write(b ? 1 : 0); | // ? : 是条件运算符:true 打印 1,false 打印 0 |
| 8 | Console.WriteLine(); | // 换行 |
| 9 | ba.Length = 10; | // 把位数改成 10 |
| 10 | Console.WriteLine(ba.Length); | // 输出 10 |
10010000 10
教程说事先不知道要几位时用它:位数可以改,把 Length 设成 10 就变成十位。
11小结
- 动态数组 ArrayList:按下标取,能自动变长。
- 哈希表 Hashtable:按键取,不保证顺序。
- 排序列表 SortedList:按键、按下标都能取,总是按键排好序。
- 栈 Stack 后进先出,队列 Queue 先进先出。
- 位数组 BitArray:每格一位,存
true或false。 - 写新代码时,优先用
List、Dictionary这些泛型集合。
下一节:ArrayList 取出来是 object,每次都要强转。能不能让集合一开始就说好只装 int?
