在咱们日常开发的 .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();

效果如下:

不要再用循环了:用 Dictionary<Guid, T> 高效访问实体对象!让查找飞起来_List

📊 测试结果(单位:毫秒)

查找方式

10 万次查找总耗时

List + FirstOrDefault

6,200ms 以上

Dictionary + TryGetValue

20ms

 ✅

✅ 结论很直接:

  • Dictionary 比 List 快了 70 倍以上!
  • 即使在 Release 模式下,JIT 优化也救不了 List 的线性查找
  • 数据越多,差距越夸张。100 万条数据时,List 已经“喘不过气”,而 Dictionary 几乎没感觉

📌 小贴士:怎么选?List 还是 Dictionary?

特性

List<T>

Dictionary<TKey, TValue>

查找性能

❌ O(n),数据越多越慢

✅ O(1),基本不随数据量增长

是否支持键值映射

❌ 只能遍历

✅ 天生为键值设计

是否允许重复元素

✅ 可以有重复 ID

❌ 键必须唯一

是否有序

✅ 保持插入顺序

❌ 不保证顺序(除非用 OrderedDictionary

是否支持索引访问

✅ list[0] 没问题

❌ 不支持

适用场景

小数据、有序、频繁遍历

高频查找、大量唯一 ID 映射

一句话总结
当你看到代码里出现 FirstOrDefault(e => e.Id == id),先别急着提交,问自己一句:
👉 这个查找会频繁执行吗?
👉 数据量会不会越来越大?
👉 能不能一开始就用 Dictionary 预处理?

有时候,就是这么一个小小的改动,能避免未来线上服务“卡到爆”、“查不动”、“QPS 直线下降”。