在咱们日常开发的 .NET 项目里,经常要处理各种实体集合,比如用户列表、订单集合、商品信息等等。而且很多时候,我们都需要根据某个唯一标识(比如 Guid
类型的 ID)去查一个具体的对象。
一开始,很多人(包括我当年)都会图省事,直接用 List<T>
或者 Collection<T>
来存这些数据。毕竟,按顺序加进去,看着也挺直观,写起来也快。
但问题来了——当数据一多,这种“遍历来查”的方式就会悄悄拖垮性能。
这篇文章就带你看看这个常见的“坑”,顺便告诉你为啥下面这种写法:
public Collection<EntityBase> Entities { get; init; }
最好换成:
private readonly IReadOnlyDictionary<Guid, EntityBase> entityLookup;
不只是性能提升,代码读起来也更清爽、更专业。
看似 harmless 的查找,其实是性能杀手
举个例子:你有一个包含 1000 个 EntityBase
实例的列表,然后在某个地方写了这么一行代码:
var result = entities.FirstOrDefault(e => e.Id == requestedId);
看起来没问题,对吧?语法正确,逻辑清晰。
但背后的问题是:每次调用 FirstOrDefault
,系统都要从头到尾把整个列表扫一遍。
也就是说:
- 查一次:时间复杂度是 O(n)
- 查 100 次?那就是 O(100n)
- 如果这代码在 API 接口里被频繁调用……那服务器压力很快就上来了
更惨的是,很多团队直到上线前压测才发现性能瓶颈,甚至等到生产环境报警了,才顺藤摸瓜找到这些“不起眼”的 FirstOrDefault
。
而且这类代码往往藏在循环里、服务调用中、或者嵌套逻辑深处,平时根本注意不到,一出事就是大问题。
更聪明的做法:用 Dictionary 实现 O(1) 查找
我们直接上对比代码,一看就懂。
❌ 不推荐写法:用 List + FirstOrDefault
public class MyService
{public List<EntityBase> Entities { get; } = new();public EntityBase? GetById(Guid id)=> Entities.FirstOrDefault(e => e.Id == id);
}
这段代码的问题不是“错”,而是“慢”。随着数据量上升,GetById
会越来越拖后腿。
✅ 推荐写法:用 Dictionary 做映射
public classMyService
{privatereadonly Dictionary<Guid, EntityBase> entityLookup;public MyService(IEnumerable<EntityBase> entities){entityLookup = entities.ToDictionary(e => e.Id);}public EntityBase? GetById(Guid id)=> entityLookup.TryGetValue(id, outvar entity) ? entity : null;
}
这么一改,好处立马体现:
- ✅ 查找速度飞起:从 O(n) 变成 O(1),常数时间搞定
- ✅ 语义更清楚:一看就知道这是“根据 ID 找对象”,不是随便遍历
- ✅ 代码更干净:不用到处写
FirstOrDefault
,减少重复代码 - ✅ 还能加安全锁:用
IReadOnlyDictionary
防止外部误改
额外加分项:用 IReadOnlyDictionary
更安全
如果你不希望别人偷偷改你的字典,可以这么写:
private readonly IReadOnlyDictionary<Guid, EntityBase> _entityLookup;
构造函数里初始化完,这个字典就“只读”了,谁也不能增删改。既安全,又让调用方清楚:这玩意儿你不该动。
⚠️ 什么时候不适合用 Dictionary?
当然,咱们也不能“一招鲜吃遍天”。Dictionary 虽好,但也不是万能的。以下几种情况,你得慎重考虑:
- ✅ 你需要保持插入顺序:比如要按添加顺序展示,或者用
list[0]
这种索引访问 - ✅ ID 不唯一:比如多个对象可能有相同的键,Dictionary 直接抛异常
- ✅ 数据量特别小:比如就 10 条数据,性能差不差几乎没感觉,用 List 更简单
所以一句话:用对场景,才是高手。
🧪 实测对比:List 和 Dictionary 到底差多少?
光说理论不够直观,咱们来点实打实的测试。
🔧 测试环境
- .NET 8
- 数据量:100 万个实体对象
- 查找次数:10 万次随机 ID
- Release 模式 + JIT 优化
🚀 测试代码(简化版)
// 准备数据
var list = new List<Entity>();
var dict = new Dictionary<Guid, Entity>();for (int i = 0; i < 1_000_000; i++)
{var entity = new Entity { Id = Guid.NewGuid() };list.Add(entity);dict[entity.Id] = entity;
}var randomIds = list.OrderBy(_ => Guid.NewGuid()).Take(100_000).Select(e => e.Id).ToList();// 测试 List 查找
var swList = Stopwatch.StartNew();
foreach (var id in randomIds)
{var result = list.FirstOrDefault(e => e.Id == id);
}
swList.Stop();// 测试 Dictionary 查找
var swDict = Stopwatch.StartNew();
foreach (var id in randomIds)
{var result = dict.TryGetValue(id, outvar e);
}
swDict.Stop();
效果如下:
📊 测试结果(单位:毫秒)
查找方式 | 10 万次查找总耗时 |
| 6,200ms 以上 |
| 20ms ✅ |
✅ 结论很直接:
- Dictionary 比 List 快了 70 倍以上!
- 即使在 Release 模式下,JIT 优化也救不了 List 的线性查找
- 数据越多,差距越夸张。100 万条数据时,List 已经“喘不过气”,而 Dictionary 几乎没感觉
📌 小贴士:怎么选?List 还是 Dictionary?
特性 |
|
|
查找性能 | ❌ O(n),数据越多越慢 | ✅ O(1),基本不随数据量增长 |
是否支持键值映射 | ❌ 只能遍历 | ✅ 天生为键值设计 |
是否允许重复元素 | ✅ 可以有重复 ID | ❌ 键必须唯一 |
是否有序 | ✅ 保持插入顺序 | ❌ 不保证顺序(除非用 |
是否支持索引访问 | ✅ | ❌ 不支持 |
适用场景 | 小数据、有序、频繁遍历 | 高频查找、大量唯一 ID 映射 |
一句话总结:
当你看到代码里出现 FirstOrDefault(e => e.Id == id)
,先别急着提交,问自己一句:
👉 这个查找会频繁执行吗?
👉 数据量会不会越来越大?
👉 能不能一开始就用 Dictionary 预处理?
有时候,就是这么一个小小的改动,能避免未来线上服务“卡到爆”、“查不动”、“QPS 直线下降”。