c#中的list

C#中的List集合,说白了,就是个能装很多东西的“动态数组”。它最大的作用就是提供一个类型安全、可变大小的列表,让你能方便地存储、管理和操作同一类型的对象序列。相比于固定大小的数组,它在使用上灵活得多,不用你操心底层扩容的细节,想加就加,想删就删。
解决方案
使用List集合,首先你需要实例化它,然后就可以通过各种方法来操作其中的元素。
using System;
using System.Collections.Generic;
public class ListExample
{
public static void Main(string[] args)
{
// 1. 声明并初始化一个List,用来存放字符串
// 你也可以在初始化时指定一个初始容量,比如 new List(100);
List names = new List();
// 2. 添加元素
names.Add("张三");
names.Add("李四");
names.Add("王五");
names.Add("张三"); // 允许重复元素
Console.WriteLine($"当前列表中有 {names.Count} 个名字。"); // Count属性获取元素数量
// 3. 访问元素(通过索引)
Console.WriteLine($"第一个名字是:{names[0]}");
Console.WriteLine($"第三个名字是:{names[2]}");
// 4. 遍历元素
Console.WriteLine("\n所有名字:");
foreach (string name in names)
Console.WriteLine(name);
// 5. 检查元素是否存在
bool containsLiSi = names.Contains("李四");
Console.WriteLine($"\n列表中包含“李四”吗? {containsLiSi}"); // 输出 True
bool containsZhaoLiu = names.Contains("赵六");
Console.WriteLine($"列表中包含“赵六”吗? {containsZhaoLiu}"); // 输出 False
// 6. 查找元素索引
int firstZhangSanIndex = names.IndexOf("张三");
Console.WriteLine($"\n第一个“张三”的索引是:{firstZhangSanIndex}"); // 输出 0
int lastZhangSanIndex = names.LastIndexOf("张三");
Console.WriteLine($"最后一个“张三”的索引是:{lastZhangSanIndex}"); // 输出 3
// 7. 移除元素
names.Remove("张三"); // 移除第一个匹配的“张三”
Console.WriteLine("\n移除一个“张三”后:");
foreach (string name in names)
Console.WriteLine(name);
Console.WriteLine($"当前列表中有 {names.Count} 个名字。"); // 数量变为3
names.RemoveAt(1); // 移除索引为1的元素(此时是“王五”)
Console.WriteLine("\n移除索引为1的元素后:");
foreach (string name in names)
Console.WriteLine(name);
Console.WriteLine($"当前列表中有 {names.Count} 个名字。"); // 数量变为2
// 8. 插入元素
names.Insert(1, "钱七"); // 在索引1的位置插入“钱七”
Console.WriteLine("\n插入“钱七”后:");
foreach (string name in names)
Console.WriteLine(name);
// 9. 排序
names.Sort(); // 默认按字母顺序排序
Console.WriteLine("\n排序后:");
foreach (string name in names)
Console.WriteLine(name);
// 10. 清空列表
names.Clear();
Console.WriteLine($"\n清空后,列表中有 {names.Count} 个名字。"); // 数量变为0
}
}
List与数组或其他集合类型(如ArrayList)相比,有哪些优势和劣势?
在我看来,List在C#集合家族里,确实是个“万金油”般的存在,尤其是在日常开发中,它的出场率极高。它的优势非常明显,但也不是没有它不擅长的地方。
与固定大小的数组(T[])相比:
-
优势:
-
动态大小: 这是最核心的优势。数组一旦创建,大小就固定了,想增减元素就得重新创建数组并复制数据,这很麻烦。
List则能自动扩容,你只管Add,它自己会处理好底层数组的扩容机制(通常是翻倍扩容),省心。 -
丰富的内置方法:
List提供了大量方便的方法,比如Add、Remove、Insert、Contains、Sort等等,这些操作数组都需要手动实现或借助LINQ。这大大提高了开发效率。
-
动态大小: 这是最核心的优势。数组一旦创建,大小就固定了,想增减元素就得重新创建数组并复制数据,这很麻烦。
-
劣势:
-
性能开销: 虽然
List内部也是基于数组实现的,但因为要处理动态扩容,当容量不足时,会创建更大的新数组并将旧数组的元素复制过去,这会带来一定的性能开销。对于元素数量极其庞大且频繁扩容的场景,这可能是个问题。另外,相比直接操作数组,它在某些简单场景下会有一点点额外的封装层级开销。 -
内存使用:
List为了预留未来扩容的空间,其内部数组的实际容量(Capacity)通常会大于当前元素数量(Count),这意味着它可能会比刚好容纳所有元素的数组占用更多内存。
-
性能开销: 虽然
与非泛型集合(如ArrayList)相比:
-
优势:
-
类型安全(Type Safety): 这是
List最大的亮点。List只能放int,List只能放string。这在编译时就能检查出类型错误,避免了运行时因类型转换失败(InvalidCastException)而导致的程序崩溃。而ArrayList可以存放任何类型的对象,这意味着你取出来的时候需要手动进行类型转换,而且编译器无法帮你检查。 -
性能提升:
ArrayList存储的是object类型,当存储值类型(如int,double)时,会发生装箱(Boxing)操作,即将值类型转换为引用类型;取出时则发生拆箱(Unboxing)。装箱和拆箱都是有性能开销的。List因为是泛型的,可以直接存储指定类型的值,避免了装箱拆箱,性能自然更好。 - 代码可读性与维护性: 明确的类型使得代码意图更清晰,也更容易理解和维护。
-
类型安全(Type Safety): 这是
总的来说,List在绝大多数情况下都是C#中处理同类型对象集合的首选。 它兼顾了易用性、类型安全和不错的性能,是一个非常均衡的选择。
在实际开发中,何时应该优先考虑使用List,又有哪些常见的陷阱需要注意?
在我的日常开发实践中,List几乎是我的默认选择,除非我明确知道有更适合特定场景的集合类型。
优先考虑使用List的场景:
- 集合大小不确定或会频繁变化: 这是最典型的应用场景。比如从数据库查询数据,结果集数量不确定;或者用户不断添加/删除购物车商品。
-
需要类型安全的集合: 当你希望集合只存储特定类型的数据,并希望在编译时就能捕获类型错误时,
List是理想选择。 -
需要方便地进行增、删、改、查、排序等常见集合操作:
List提供了丰富的API来满足这些需求,无需自己实现。 -
作为方法的参数或返回值: 当你需要传递一个可变长度的同类型数据序列时,
List比数组更灵活,也比IEnumerable在某些需要具体操作的场景下更直接。 -
需要将数据转换为数组或其他集合:
List可以很方便地通过ToArray()或ToList()(如果从其他IEnumerable转换)进行转换。
常见的陷阱和注意事项:
-
在
foreach循环中修改集合: 这是一个非常经典的错误。当你正在用foreach循环遍历List时,如果尝试添加或移除元素,会导致运行时错误(InvalidOperationException: Collection was modified; enumeration operation may not execute.)。-
应对方案:
- 如果需要删除元素,可以从后往前遍历
for循环。 - 创建一个副本进行遍历,然后修改原集合。
- 使用LINQ的
Where等方法筛选出需要保留的元素,然后重新赋值给列表。 - 对于删除操作,可以先收集要删除的元素,再统一删除。
- 如果需要删除元素,可以从后往前遍历
-
应对方案:
-
Capacity与Count的混淆:Count是列表中实际元素的数量,而Capacity是List内部数组的当前容量。List会自动扩容,但如果你预先知道大概的元素数量,最好在初始化时指定Capacity,比如new List,这样可以避免多次不必要的扩容操作,提高性能。(1000) -
对引用类型操作的理解:
List存储的是引用类型对象的引用。这意味着如果你修改了列表中某个引用类型对象内部的属性,那么原对象也会被修改。如果你想在列表中存储对象的独立副本,需要自行实现深拷贝。 -
频繁在列表头部或中部插入/删除大量元素: 尽管
List是动态的,但在列表的头部或中部插入/删除元素时,其内部需要将后续所有元素都移动,这在元素数量庞大时会产生显著的性能开销。如果你的场景是频繁在两端操作,或者需要高效的插入/删除,LinkedList(链表)可能是更好的选择。但对于大多数应用来说,List的性能已经足够好。 -
Clear()与TrimExcess():Clear()方法只会将Count设置为0,但不会释放内部数组的内存,Capacity保持不变。如果你确定列表在很长一段时间内不会再使用,或者会变得非常小,并且内存是一个瓶颈,可以调用TrimExcess()来将Capacity调整为与Count相同(或更接近),以释放多余内存。但请注意,TrimExcess()本身也是一个有开销的操作,因为它可能需要重新分配和复制数组。所以,除非有明确的内存优化需求,否则不建议频繁调用。
如何优化List的性能,特别是在处理大量数据时?
处理大量数据时,List的性能优化就显得尤为重要,这不仅仅是代码写得好不好看的问题,更是直接影响用户体验和系统资源占用的关键。
-
预设初始容量(Pre-allocate Capacity): 当你知道
List大概会包含多少元素时,在初始化时就指定一个合适的初始容量,这是最简单也最有效的优化手段之一。// 假设你知道大概会有10000个元素 List
largeList = new List (10000); // 这样在添加前10000个元素时,就避免了多次内部数组的重新分配和数据复制。 如果不预设,
List在内部数组满时会自动扩容(通常是当前容量的两倍),这个扩容过程会涉及创建一个更大的新数组并将旧数组的元素复制过去,这在数据量大时会非常耗时。 -
使用
AddRange()替代循环Add(): 如果你有一批数据(比如从数据库查询出来的IEnumerable)需要一次性添加到List中,使用AddRange()会比循环调用Add()方法更高效。AddRange()会一次性计算所需的总容量,可能只进行一次扩容操作,而循环Add()可能会触发多次扩容。List
numbers = new List (); List newNumbers = new List { 1, 2, 3, 4, 5 }; // 推荐:一次性添加所有元素 numbers.AddRange(newNumbers); // 避免:循环添加(可能导致多次扩容) // foreach (int num in newNumbers) // { // numbers.Add(num); // } -
减少中间插入和删除操作: 前面也提到了,
List在内部是基于数组的。在列表的中间位置插入或删除元素,会导致其后的所有元素都需要进行内存移动(Array.Copy),这个操作的复杂度是O(n),n是移动的元素数量。对于少量数据影响不大,但对于百万级甚至千万级的数据量,频繁的中间操作会导致性能急剧下降。- 如果你的业务逻辑确实需要频繁在中间进行插入删除,那么可能需要重新考虑数据结构,比如使用
LinkedList(链表),它的插入删除是O(1)复杂度,但随机访问是O(n)。 - 如果可以,尽量将插入操作集中在列表末尾,或者先收集所有数据再统一处理(比如排序后批量插入)。
- 如果你的业务逻辑确实需要频繁在中间进行插入删除,那么可能需要重新考虑数据结构,比如使用
-
合理利用
TrimExcess()(谨慎使用): 当List的Capacity远大于Count,并且你确定在短期内不会再向列表中添加大量元素时,可以调用TrimExcess()来将内部数组的容量调整为实际元素数量。这可以释放多余的内存,对于内存敏感的应用非常有用。List
bigList = new List (1000000); // ... 添加大量元素 ... // 假设最后只剩下1000个元素 // bigList.RemoveAll(item => someCondition); Console.WriteLine($"Before TrimExcess: Capacity = {bigList.Capacity}, Count = {bigList.Count}"); bigList.TrimExcess(); // 调整容量 Console.WriteLine($"After TrimExcess: Capacity = {bigList.Capacity}, Count = {bigList.Count}"); 注意:
TrimExcess()本身也是一个耗时的操作,因为它涉及到新的内存分配和数据复制。因此,不应频繁调用,只在确定列表大小已稳定且内存成为瓶颈时使用。 -
使用LINQ时的性能考量: LINQ提供了一种非常便利的方式来查询和操作集合,但某些LINQ操作在处理大量数据时可能会带来额外的开销。
-
避免不必要的
ToList(): 如果你已经有一个List,并且只是想对其进行过滤或投影,然后继续以IEnumerable的形式使用,就没必要每次都调用ToList()。ToList()会创建一个新的列表副本,增加内存和CPU开销。 -
延迟执行(Deferred Execution): LINQ查询通常是延迟执行的,这意味着它们只在真正需要结果时才执行。这通常是好事,但如果你多次枚举同一个查询结果,每次都会重新执行查询。对于计算量大的查询,可以考虑在第一次执行后调用
ToList()或ToArray()将其结果缓存起来。
-
避免不必要的
-
并行处理(Parallel Processing): 对于非常大的
List,如果你需要对其进行计算密集型操作(如复杂的数据转换或分析),可以考虑使用PLINQ(Parallel LINQ)或Parallel.ForEach来并行处理数据,充分利用多核CPU的优势,显著缩短处理时间。// 假设有一个很大的List
需要进行复杂计算 List dataPoints = new List (10000000); // ... 填充数据 ... // 使用Parallel.ForEach并行处理 Parallel.ForEach(dataPoints, point => { // 执行耗时操作 // Process(point); }); // 或者使用PLINQ // var processedResults = dataPoints.AsParallel().Select(point => Process(point)).ToList(); 但并行处理并非万能药,它会引入线程同步、任务调度等开销,对于小数据集反而可能更慢。只有在数据量足够大且计算确实是瓶颈时才考虑。









