Menu

C# List 用法:Add、Remove、Contains、Find 与 Sort

List<T> 是 C# 中可以自动扩容的数组。学习如何创建列表,添加和插入元素,按值、索引或条件删除,用 Contains 和 Find 查找,按属性排序,以及避免在 foreach 中修改列表导致的错误。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

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 很快。

Coddy 编程语言插图

和 Coddy 一起学编程

开始