当前位置: 首页 > news >正文

揭开.NET Core 中 ToList () 与 ToArray () 的面纱:从原理到抉择

目录

先从底层实现看差异

关键差异:内存占用与性能

内存占用

性能表现

结合 Linq 的场景化分析

1. Linq 查询后需 “二次加工”

2. Linq 查询结果 “只读不改”

3. Linq 查询 “长度未知” 的场景

性能比较测评:用数据说话

测试场景 1:小数据量 Linq 查询转换(1000 条)

测试场景 2:大数据量 Linq 查询转换(100 万条)

测试场景 3:Linq 转换后 “频繁添加元素”

测试场景 4:Linq 转换后 “只读访问”

总结


在.NET Core 开发中,我们经常会与集合打交道,而 ToList () 和 ToArray () 这两个方法更是频繁出现在代码里。它们都能将 IEnumerable 转换为具体的集合类型,但很多开发者在选择时会感到困惑 —— 到底该用哪个?其实,这两个方法看似相似,底层实现和适用场景却有不小差异,选对了能让代码更高效,选错则可能造成性能或内存上的浪费。今天我们来聊聊这两者的区别,以及如何根据实际场景做出选择。

先从底层实现看差异

要搞懂 ToList () 和 ToArray () 的区别,先得看看它们的底层是怎么实现的。

以.NET Core 的源码为例,当我们调用 ToList () 时,它会创建一个 List对象。List内部维护着一个数组来存储元素,不过它会预留一定的 “缓冲空间”。比如当你往 List 里添加元素时,若当前数组容量不够,它会自动扩容(通常是翻倍扩容),所以即便你转换的序列有 N 个元素,List内部数组的长度可能会大于 N,这些多出来的空间就是为了后续可能的添加、删除操作做准备。

而 ToArray () 呢?它会直接创建一个长度恰好等于序列元素个数的数组。也就是说,如果你转换的序列有 N 个元素,得到的数组长度就是 N,不会有额外的缓冲空间。这是因为数组一旦创建,长度就固定了,不像 List那样可以动态扩容,所以 ToArray () 会精确分配所需的内存。

关键差异:内存占用与性能

从底层实现就能延伸出两者在内存占用和性能上的关键差异,这也是我们选择时的重要依据。

内存占用

List因为有缓冲空间,内存占用通常会比数组多。比如你有一个包含 10 个元素的序列,用 ToList () 得到的 List,内部数组可能是 16(假设初始扩容到 16),那多出来的 6 个元素位置就占用了额外内存;而用 ToArray () 得到的数组长度就是 10,内存利用更紧凑。如果处理的是大量数据,这种内存差异可能会比较明显。

不过这里要注意,List的 TrimExcess () 方法可以释放多余的缓冲空间,让内部数组长度和元素个数一致,但调用这个方法会有一定的性能开销,而且之后再添加元素又会触发扩容,所以不能随意滥用。

性能表现

性能方面要分创建时和使用时来看。

创建时,ToList () 因为不需要精确计算最终长度(缓冲空间的存在),在某些情况下可能比 ToArray () 快一点。比如当序列的长度不明确时,ToArray () 可能需要先遍历一次获取长度,再分配内存,而 ToList () 可以边遍历边添加,遇到容量不够时再扩容。但如果是已知长度的序列,两者的创建速度差异可能不大。

使用时,数组和 List在元素访问上性能差不多,都是 O (1) 的时间复杂度。但如果涉及到频繁的添加、删除操作,List就比数组更合适了,因为数组长度固定,添加删除需要重新创建数组并复制元素,成本很高;而 List有缓冲空间,在缓冲空间足够时,添加操作成本很低。

结合 Linq 的场景化分析

Linq(语言集成查询)是.NET 中处理集合的常用工具,而 ToList () 和 ToArray () 常作为 Linq 查询的 “收尾操作”—— 因为 Linq 采用延迟执行机制,只有调用这两个方法(或 Count ()、First () 等触发执行的方法)时,查询才会真正运行。在 Linq 场景中,两者的选择更需结合查询逻辑和后续操作,我们分几种典型情况来看:

1. Linq 查询后需 “二次加工”

如果 Linq 查询后还要对结果做添加、删除、修改元素等操作,优先用 ToList ()

比如从数据库查询用户列表后,需要在内存中补充 “是否为 VIP” 的标记:

var query = dbContext.Users.Where(u => u.RegTime > DateTime.Now.AddYears(-1));
// 转换为List,后续添加标记更方便
var userList = query.ToList();
foreach (var user in userList)
{user.IsVip = user.Points > 1000;// 若需补充元素:userList.Add(new User(...));
}

这里用 ToList () 的核心原因是 List支持动态修改 —— 如果用 ToArray (),后续若需添加元素,得先创建新数组再复制元素(如userArray = userArray.Concat(newUser).ToArray()),不仅代码繁琐,性能也会因频繁数组重构下降。

2. Linq 查询结果 “只读不改”

如果 Linq 查询后仅用于遍历、展示或传递(无修改操作),优先用 ToArray ()

比如从内存集合中筛选订单,仅用于前端展示:

var orders = orderList.Where(o => o.Status == OrderStatus.Paid).Select(o => new { o.Id, o.Amount, o.BuyerName }).ToArray(); // 仅读取,用数组更省内存
// 直接传递给前端或遍历展示
foreach (var order in orders)
{Console.WriteLine($"订单{order.Id}:{order.Amount}元");
}

此时 ToArray () 的 “无缓冲内存” 优势会体现 —— 尤其当查询结果数据量大时(如 10 万条以上),数组比 List节省的缓冲空间(通常是元素个数的 20%-50%)能明显降低内存占用。

3. Linq 查询 “长度未知” 的场景

如果 Linq 查询中用了 Where、Distinct 等可能改变序列长度的操作(无法提前确定结果个数),ToList () 更合适

比如筛选随机生成的数字序列中大于 0 的值:

var randomNumbers = Enumerable.Range(0, 10000).Select(_ => Random.Shared.Next(-100, 100));
// 筛选后长度未知(可能5000个,也可能6000个)
var positiveNumbers = randomNumbers.Where(n => n > 0).ToList();

这类场景下,ToList () 无需先 “算长度再分配内存”,而是边遍历边动态扩容,避免了 ToArray () 可能的 “两次遍历”(一次算长度、一次填数据),创建速度会更快。反之,若 Linq 用了 Take (100) 这类明确长度的操作(如Where(...).Take(100)),ToArray () 也能高效分配内存,此时两者性能差异不大。

性能比较测评:用数据说话

光说理论不够直观,我们通过 4 个常见场景的测试,看看 ToList () 和 ToArray () 的实际性能差异。测试环境:.NET Core 6.0,CPU i5-12400,内存 16GB,测试数据为int类型(简化计算,其他类型趋势一致)。

测试场景 1:小数据量 Linq 查询转换(1000 条)

操作:用 Linq 从 1000 条随机数中筛选大于 0 的值,分别用 ToList () 和 ToArray () 转换,重复 1000 次取平均耗时和内存。

方法

平均耗时(毫秒)

内存占用(字节)

ToList()

0.08

4416

ToArray()

0.07

4096

结论:小数据量下两者性能接近,ToArray () 因内存紧凑(4096 字节 = 1000×4 字节,无缓冲),内存占用略低;ToList () 因缓冲空间(内部数组长度通常是 1024,1024×4=4096 字节?不对,这里 1000 条元素,List初始容量可能是 1000,扩容后可能 1000,所以差异小),耗时差异可忽略。

测试场景 2:大数据量 Linq 查询转换(100 万条)

操作:用 Linq 从 100 万条随机数中筛选大于 0 的值(约 50 万条结果),分别转换,重复 100 次取平均。

方法

平均耗时(毫秒)

内存占用(字节)

ToList()

28.3

2,097,152

ToArray()

29.1

2,000,000

结论:耗时接近(ToList () 略快,因无需提前算长度),但内存差异明显 ——ToList () 内部数组为了缓冲,容量会扩容到最近的 “2 的幂”(如 50 万条元素,List会扩容到 524,288,占用 524288×4=2,097,152 字节),而 ToArray () 仅需 500,000×4=2,000,000 字节,节省约 5% 内存。

测试场景 3:Linq 转换后 “频繁添加元素”

操作:Linq 筛选出 1000 条元素后,再连续添加 1000 条新元素,测总耗时。

方法

总耗时(毫秒)

关键原因

ToList()

0.12

缓冲空间足够,直接添加

ToArray()

1.85

需多次创建新数组并复制

结论:ToList () 优势明显 —— 因为初始转换后 List有缓冲空间(比如 1000 条元素,内部数组可能有 1024 容量),添加 1000 条时仅需 1-2 次扩容;而 ToArray () 每次添加都要通过Concat+ToArray()重构数组,1000 次添加需执行 1000 次数组复制,耗时是 ToList () 的 15 倍以上。

测试场景 4:Linq 转换后 “只读访问”

操作:Linq 转换 100 万条元素后,循环遍历 100 次(仅读元素),测总耗时。

方法

总耗时(毫秒)

关键原因

ToList()

89.2

内部数组访问,与数组性能一致

ToArray()

88.7

直接数组访问

结论:两者访问性能几乎无差异 —— 因为 List本质是 “数组 + 封装”,元素访问也是直接操作内部数组(list[i]等价于list._items[i]),所以遍历速度和数组基本一致。

总结

结合 Linq 场景和性能测试,ToList () 和 ToArray () 的选择可以更清晰:

  • 若需修改结果(添加、删除元素),或 Linq 查询长度未知,选 ToList ()—— 动态扩容和修改便捷性是核心优势;
  • 只读不修改,或对内存占用敏感(尤其大数据量),选 ToArray ()—— 无缓冲内存和紧凑存储更高效;
  • 小数据量、无特殊需求时,两者差异可忽略,按代码习惯选即可。

本质上,它们的核心区别是 “设计目标”:ToList () 是 “为修改而生的动态集合”,ToArray () 是 “为高效存储而生的静态集合”。结合 Linq 的查询逻辑和后续操作需求,再参考性能测试的趋势,就能轻松做出更合理的选择。

http://www.dtcms.com/a/355556.html

相关文章:

  • ansible常用命令的简单练习
  • Linux系统 -- 多线程的控制(互斥与同步)
  • 数学思维好题(冯哈伯公式)-平方根下取整求和
  • 个人博客运行3个月记录
  • 了解ADS中信号和电源完整性的S参数因果关系
  • Typora 教程:从零开始掌握 Markdown 高效写作
  • MySQL事务ACID特性
  • JavaScript中的BOM,DOM和事件
  • 英语单词:ad-hoc
  • BugKu Web渗透之成绩查询
  • 白杨SEO:网站优化怎么做?应用举例和适合哪些企业做?参考
  • 速成Javascript(二)
  • 新书速览|SQL Server运维之道
  • 【第三方网站运行环境测试:服务器配置(如Nginx/Apache)的WEB安全测试重点】
  • 激活函数篇(3):Softmax
  • maven scope 详解
  • 通信原理实验之线性均衡器-迫零算法
  • dht11传感器总结
  • [灵动微电子 MM32BIN560CN MM32SPIN0280]读懂电机MCU之串口DMA
  • 【C++游记】子承父业——乃继承也
  • 91美剧网官网入口 - 最新美剧资源在线观看网站
  • 保姆级教程 | 在Ubuntu上部署Claude Code Plan Mode全过程
  • 【论文阅读】MotionXpert:基于肌电信号的优化下肢运动检测分类
  • Spring事务管理机制深度解析:从JDBC基础到Spring高级实现
  • [灵动微电子MM32SPIN0280]从灵动微电子看电机专用MCU
  • Deeplizard 深度学习课程(五)—— 模型训练
  • 数据结构01:顺序表
  • react Antd Table 多选大数据量 UI渲染很慢的解决方案
  • 每日五个pyecharts可视化图表日历图和箱线图:从入门到精通
  • ChatGPT登录,拒绝访问,错误1020解决办法