c#中的list<t>是动态数组,提供类型安全、可变大小的列表,便于存储和操作同类型对象。1. 优势:动态扩容,无需手动管理;内置丰富方法如add、remove、sort等;类型安全避免运行时错误;性能优于arraylist,避免装箱拆盒。2. 劣势:频繁扩容带来性能开销;内存占用可能高于固定数组;在中间插入/删除效率低。3. 使用场景:集合大小变化频繁、需类型安全、需便捷操作、作为方法参数或转换为其他结构。4. 常见陷阱:foreach中修改集合会抛异常;混淆capacity与count;引用类型修改影响原对象;频繁中部操作性能差;clear不释放内存。5. 性能优化:预设初始容量减少扩容;使用addrange代替循环add;减少中间插入删除;合理使用trimexcess;慎用linq的tolist;大数据考虑并行处理。

C#中的List<T>集合,说白了,就是个能装很多东西的“动态数组”。它最大的作用就是提供一个类型安全、可变大小的列表,让你能方便地存储、管理和操作同一类型的对象序列。相比于固定大小的数组,它在使用上灵活得多,不用你操心底层扩容的细节,想加就加,想删就删。
使用List<T>集合,首先你需要实例化它,然后就可以通过各种方法来操作其中的元素。
using System;
using System.Collections.Generic;
public class ListExample
{
public static void Main(string[] args)
{
// 1. 声明并初始化一个List<string>,用来存放字符串
// 你也可以在初始化时指定一个初始容量,比如 new List<string>(100);
List<string> names = new List<string>();
// 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<T>与数组或其他集合类型(如ArrayList)相比,有哪些优势和劣势?在我看来,List<T>在C#集合家族里,确实是个“万金油”般的存在,尤其是在日常开发中,它的出场率极高。它的优势非常明显,但也不是没有它不擅长的地方。
与固定大小的数组(T[])相比:
List<T>则能自动扩容,你只管Add,它自己会处理好底层数组的扩容机制(通常是翻倍扩容),省心。List<T>提供了大量方便的方法,比如Add、Remove、Insert、Contains、Sort等等,这些操作数组都需要手动实现或借助LINQ。这大大提高了开发效率。List<T>内部也是基于数组实现的,但因为要处理动态扩容,当容量不足时,会创建更大的新数组并将旧数组的元素复制过去,这会带来一定的性能开销。对于元素数量极其庞大且频繁扩容的场景,这可能是个问题。另外,相比直接操作数组,它在某些简单场景下会有一点点额外的封装层级开销。List<T>为了预留未来扩容的空间,其内部数组的实际容量(Capacity)通常会大于当前元素数量(Count),这意味着它可能会比刚好容纳所有元素的数组占用更多内存。与非泛型集合(如ArrayList)相比:
List<T>最大的亮点。List<int>只能放int,List<string>只能放string。这在编译时就能检查出类型错误,避免了运行时因类型转换失败(InvalidCastException)而导致的程序崩溃。而ArrayList可以存放任何类型的对象,这意味着你取出来的时候需要手动进行类型转换,而且编译器无法帮你检查。ArrayList存储的是object类型,当存储值类型(如int, double)时,会发生装箱(Boxing)操作,即将值类型转换为引用类型;取出时则发生拆箱(Unboxing)。装箱和拆箱都是有性能开销的。List<T>因为是泛型的,可以直接存储指定类型的值,避免了装箱拆箱,性能自然更好。总的来说,List<T>在绝大多数情况下都是C#中处理同类型对象集合的首选。 它兼顾了易用性、类型安全和不错的性能,是一个非常均衡的选择。
在我的日常开发实践中,List<T>几乎是我的默认选择,除非我明确知道有更适合特定场景的集合类型。
优先考虑使用List<T>的场景:
List<T>是理想选择。List<T>提供了丰富的API来满足这些需求,无需自己实现。List<T>比数组更灵活,也比IEnumerable<T>在某些需要具体操作的场景下更直接。List<T>可以很方便地通过ToArray()或ToList()(如果从其他IEnumerable转换)进行转换。常见的陷阱和注意事项:
foreach循环中修改集合: 这是一个非常经典的错误。当你正在用foreach循环遍历List<T>时,如果尝试添加或移除元素,会导致运行时错误(InvalidOperationException: Collection was modified; enumeration operation may not execute.)。for循环。Where等方法筛选出需要保留的元素,然后重新赋值给列表。Capacity与Count的混淆: Count是列表中实际元素的数量,而Capacity是List<T>内部数组的当前容量。List<T>会自动扩容,但如果你预先知道大概的元素数量,最好在初始化时指定Capacity,比如new List<MyObject>(1000),这样可以避免多次不必要的扩容操作,提高性能。List<T>存储的是引用类型对象的引用。这意味着如果你修改了列表中某个引用类型对象内部的属性,那么原对象也会被修改。如果你想在列表中存储对象的独立副本,需要自行实现深拷贝。List<T>是动态的,但在列表的头部或中部插入/删除元素时,其内部需要将后续所有元素都移动,这在元素数量庞大时会产生显著的性能开销。如果你的场景是频繁在两端操作,或者需要高效的插入/删除,LinkedList<T>(链表)可能是更好的选择。但对于大多数应用来说,List<T>的性能已经足够好。Clear()与TrimExcess(): Clear()方法只会将Count设置为0,但不会释放内部数组的内存,Capacity保持不变。如果你确定列表在很长一段时间内不会再使用,或者会变得非常小,并且内存是一个瓶颈,可以调用TrimExcess()来将Capacity调整为与Count相同(或更接近),以释放多余内存。但请注意,TrimExcess()本身也是一个有开销的操作,因为它可能需要重新分配和复制数组。所以,除非有明确的内存优化需求,否则不建议频繁调用。处理大量数据时,List<T>的性能优化就显得尤为重要,这不仅仅是代码写得好不好看的问题,更是直接影响用户体验和系统资源占用的关键。
预设初始容量(Pre-allocate Capacity):
当你知道List<T>大概会包含多少元素时,在初始化时就指定一个合适的初始容量,这是最简单也最有效的优化手段之一。
// 假设你知道大概会有10000个元素 List<MyObject> largeList = new List<MyObject>(10000); // 这样在添加前10000个元素时,就避免了多次内部数组的重新分配和数据复制。
如果不预设,List<T>在内部数组满时会自动扩容(通常是当前容量的两倍),这个扩容过程会涉及创建一个更大的新数组并将旧数组的元素复制过去,这在数据量大时会非常耗时。
使用AddRange()替代循环Add():
如果你有一批数据(比如从数据库查询出来的IEnumerable<T>)需要一次性添加到List<T>中,使用AddRange()会比循环调用Add()方法更高效。AddRange()会一次性计算所需的总容量,可能只进行一次扩容操作,而循环Add()可能会触发多次扩容。
List<int> numbers = new List<int>();
List<int> newNumbers = new List<int> { 1, 2, 3, 4, 5 };
// 推荐:一次性添加所有元素
numbers.AddRange(newNumbers);
// 避免:循环添加(可能导致多次扩容)
// foreach (int num in newNumbers)
// {
// numbers.Add(num);
// }减少中间插入和删除操作:
前面也提到了,List<T>在内部是基于数组的。在列表的中间位置插入或删除元素,会导致其后的所有元素都需要进行内存移动(Array.Copy),这个操作的复杂度是O(n),n是移动的元素数量。对于少量数据影响不大,但对于百万级甚至千万级的数据量,频繁的中间操作会导致性能急剧下降。
LinkedList<T>(链表),它的插入删除是O(1)复杂度,但随机访问是O(n)。合理利用TrimExcess()(谨慎使用):
当List<T>的Capacity远大于Count,并且你确定在短期内不会再向列表中添加大量元素时,可以调用TrimExcess()来将内部数组的容量调整为实际元素数量。这可以释放多余的内存,对于内存敏感的应用非常有用。
List<string> bigList = new List<string>(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<T>,并且只是想对其进行过滤或投影,然后继续以IEnumerable<T>的形式使用,就没必要每次都调用ToList()。ToList()会创建一个新的列表副本,增加内存和CPU开销。ToList()或ToArray()将其结果缓存起来。并行处理(Parallel Processing):
对于非常大的List<T>,如果你需要对其进行计算密集型操作(如复杂的数据转换或分析),可以考虑使用PLINQ(Parallel LINQ)或Parallel.ForEach来并行处理数据,充分利用多核CPU的优势,显著缩短处理时间。
// 假设有一个很大的List<double>需要进行复杂计算
List<double> dataPoints = new List<double>(10000000);
// ... 填充数据 ...
// 使用Parallel.ForEach并行处理
Parallel.ForEach(dataPoints, point =>
{
// 执行耗时操作
// Process(point);
});
// 或者使用PLINQ
// var processedResults = dataPoints.AsParallel().Select(point => Process(point)).ToList();但并行处理并非万能药,它会引入线程同步、任务调度等开销,对于小数据集反而可能更慢。只有在数据量足够大且计算确实是瓶颈时才考虑。
以上就是C#中的List集合有什么作用?如何使用?的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号