List<T> 是一个有序集合,会随着你添加和删除元素而增长或缩小。它位于 System.Collections.Generic 中,可以像数组一样用索引访问(list[0]),在 C# 中需要存放“一组东西”时,它就是默认的选择。
创建列表并添加元素
尖括号中的类型参数说明列表里存放什么:List<string>、List<int>、List<Order>。
输出:
2
Zoe, Ana, Ben, Chloe, Dev
Ana
Ana B.
3
Count 是属性,而不是方法(数组用的是 Length,LINQ 有一个 Count() 方法;在列表上请使用这个属性)。Insert 会把后面的每个元素都往后挪一位,所以在大列表的开头插入元素,耗时与列表大小成正比。在末尾 Add 才是开销小的操作。
删除元素
根据你对要删除的元素了解多少,有四种删除方式:
输出:
True: 85, 62, 40, 91, 55
False
62, 40, 91, 55
2 removed: 62, 91
Caught ArgumentOutOfRangeException
0
Remove 接受一个值,只删除第一个匹配项。要删除某个值的所有副本,请使用 RemoveAll(s => s == 40)。在列表上使用无效的索引会抛出 ArgumentOutOfRangeException(数组则抛出 IndexOutOfRangeException),list[i] 和 RemoveAt(i) 都是如此。RemoveRange(index, count) 用于删除一段连续的元素。
Count 与 Capacity
列表把元素保存在一个内部数组中。Capacity 是这个数组的大小;Count 是已经使用的位置个数。当 Count 达到 Capacity 时,下一次 Add 会分配一个两倍大的数组,并把所有元素复制过去。
输出:
Count 0, Capacity 0
Count 1, Capacity 4
Count 2, Capacity 4
Count 3, Capacity 4
Count 4, Capacity 4
Count 5, Capacity 8
Count 6, Capacity 8
Count 7, Capacity 8
Count 8, Capacity 8
Count 9, Capacity 16
Count 0, Capacity 1000
容量翻倍意味着大多数 Add 调用只是往空位里写入一个值。如果你大致知道会有多少个元素,可以把这个数字传给构造函数,省去中间的多次复制。注意 new List<int>(1000) 的 Count 仍然是 0:对它访问 list[0] 会抛出异常。容量是空间,不是元素。
查找:Contains、IndexOf、Find、Exists
输出:
True
2
-1
32.00
2 cheap items
False
2
True
False
Find 返回第一个匹配项;没有匹配时返回该类型的默认值(类为 null),所以使用结果之前要先检查。最后一行打印 False,是因为 Contains 使用 Equals,而没有重写 Equals 的类比较的是引用:一个字段相同的新 Product 是另一个对象。请改用 Exists 或 Find 按属性查找,或者在类中重写 Equals 和 GetHashCode。
这些方法每一个都从头开始遍历列表,所以每次调用都是 O(n)。如果需要按键反复查找,Dictionary 能在常数时间内给出答案。
对列表排序
Sort() 按元素的自然顺序原地排序。要按别的规则排序,就传入一个比较用的 Lambda。
输出:
1, 2, 5, 9
9, 5, 2, 1
Ben 95, Ana 120, Chloe 150
Chloe 150, Ben 95, Ana 120
Ben 95, Ana 120, Chloe 150
比较函数在 a 应该排在前面时返回负数,在 b 应该排在前面时返回正数,两者相等时返回零;CompareTo 返回的正是这样的结果。Sort 和 LINQ 的 OrderBy 有两点区别在实践中很重要:
Sort会修改列表;OrderBy返回一个新序列,不动原列表。Sort不是稳定排序:比较结果相等的元素可能会交换位置。OrderBy是稳定的,ThenBy可以添加第二个排序键:players.OrderBy(p => p.Score).ThenBy(p => p.Name)。
对自定义类的列表调用不带比较函数的 Sort() 会抛出 InvalidOperationException,因为列表不知道如何给这些元素排序。你可以传入一个比较函数,或者在类上实现 IComparable<T>。
Reverse() 会原地反转列表。由于 System.Linq 也定义了一个 Reverse 扩展方法,在 List<T> 上调用 list.Reverse() 会选中原地反转的版本,它没有返回值。
遍历,以及在遍历时删除
foreach 按顺序读取每个元素。for 循环还能给你索引。你不能做的,是在 foreach 遍历同一个列表的过程中添加或删除元素:
输出:
Caught InvalidOperationException
120, 80, 60
120, 80, 60
这个异常是 InvalidOperationException,消息为“Collection was modified; enumeration operation may not execute.”。正序的 for 循环不会抛出异常,但每删除一个元素,它都会悄悄跳过紧跟其后的那个元素,因为后面的所有元素都往前挪了一个索引。倒序遍历就能避免这个问题。第三种解决办法是遍历一个副本:foreach (var t in orders.ToList())。
在 foreach 中修改某个元素的属性(例如 order.Status = "sent")是没问题的。添加或删除元素会破坏枚举器,通过索引器替换元素(orders[i] = 0)也一样。
列表与数组的相互转换
输出:
4
3
4, 4, 4, 4
Oslo / Lima / Pune / Kyiv
两个方向的转换都会把元素复制到新的存储空间,所以之后修改列表不会影响数组。ConvertAll 是列表自带的版本,相当于 LINQ 的 Select(...).ToList()。用 Console.WriteLine(list) 打印列表只会显示类型名(System.Collections.Generic.List`1[System.String]);请使用 string.Join。
速查表
| 任务 | 代码 |
|---|---|
| 创建 | var l = new List<int>(); 或 new List<int> { 1, 2 } |
| 在末尾添加 | l.Add(x)、l.AddRange(items) |
| 在指定位置插入 | l.Insert(i, x) |
| 删除第一个匹配项 | l.Remove(x)(返回 bool) |
| 按索引删除 | l.RemoveAt(i) |
| 按条件删除 | l.RemoveAll(x => ...)(返回删除个数) |
| 元素个数 | l.Count |
| 是否包含 | l.Contains(x)、l.Exists(x => ...) |
| 查找 | l.Find(...)、l.FindAll(...)、l.FindIndex(...) |
| 原地排序 | l.Sort()、l.Sort((a, b) => ...) |
| 排序后的副本 | l.OrderBy(x => ...).ToList() |
| 转为数组 | l.ToArray() |
常见错误
- 在
foreach中删除元素。 会抛出InvalidOperationException;请使用RemoveAll或倒序的for循环。 - 以为
Remove(x)会删除所有副本。 它只删除第一个;请使用RemoveAll。 - 对没有重写
Equals的对象使用Contains。 它比较的是引用;请用Exists按属性查找。 - 对指定了容量的新列表直接用索引访问。
new List<int>(10)是空的;要先添加元素。 - 以为
Sort会保持相等元素的顺序。 它不会;当相等元素之间的顺序很重要时,请使用OrderBy。 - 通过赋值共享列表,然后对变化感到意外。
var copy = list;仍然是同一个列表;new List<T>(list)才是副本。
常见问题
C# 中如何向 List 添加元素?
list.Add(item) 在末尾追加一个元素,list.AddRange(otherCollection) 追加多个元素,list.Insert(index, item) 把元素放到指定位置,并把后面的元素依次后移。你也可以在创建列表时就填充它:var names = new List<string> { "Ana", "Ben" };。
C# 中如何从 List 删除元素?
Remove(value) 删除第一个匹配的元素,找到时返回 true。RemoveAt(index) 按位置删除。RemoveAll(x => condition) 删除所有匹配的元素,并返回删除的个数。Clear() 清空整个列表。
C# 中如何判断 List 是否包含某个值?
list.Contains(value) 返回 true 或 false。它用 Equals 进行比较,所以对于你自己定义的类,除非类重写了 Equals,否则比较的是引用。要按属性判断,请使用 list.Exists(p => p.Name == "Ana") 或 LINQ 的 list.Any(...)。
C# 中如何按属性对 List 排序?
给 Sort 传入一个比较函数:people.Sort((a, b) => a.Age.CompareTo(b.Age)) 会原地排序。交换 a 和 b 就是降序。也可以使用 LINQ,它返回一个新的有序序列,并且让相等的元素保持原来的顺序:people.OrderBy(p => p.Age).ToList()。
为什么会出现“Collection was modified; enumeration operation may not execute”错误?
你在 foreach 遍历同一个列表的过程中添加、删除或替换(list[i] = x)了元素,枚举器在下一步就会抛出 InvalidOperationException。请使用 list.RemoveAll(condition),用 for 循环倒序遍历,或者遍历一个副本,比如 list.ToList()。
Count 和 Capacity 有什么区别?
Count 是列表中元素的个数。Capacity 是内部数组的大小,至少等于 Count。当数组装满时,列表会分配一个两倍大的新数组并把元素复制过去,所以平均来看 Add 很快。