
在咱们日常开发的 .NET 项目里,经常要处理各种实体集合,比如用户列表、订单集合、商品信息等等。而且很多时候,我们都需要根据某个唯一标识(比如 Guid 类型的 ID)去查一个具体的对象。
一开始,很多人(包括我当年)都会图省事,直接用 List<T> 或者 Collection<T> 来存这些数据。毕竟,按顺序加进去,看着也挺直观,写起来也快。
但问题来了——当数据一多,这种“遍历来查”的方式就会悄悄拖垮性能。
这篇文章就带你看看这个常见的“坑”,顺便告诉你为啥下面这种写法:
public Collection<EntityBase> Entities { get; init; }
最好换成:
private readonly IReadOnlyDictionary<Guid, EntityBase> entityLookup;
不只是性能提升,代码读起来也更清爽、更专业。
举个例子:你有一个包含 1000 个 EntityBase 实例的列表,然后在某个地方写了这么一行代码:
var result = entities.FirstOrDefault(e => e.Id == requestedId);
看起来没问题,对吧?语法正确,逻辑清晰。
但背后的问题是:每次调用 FirstOrDefault,系统都要从头到尾把整个列表扫一遍。
也就是说:
更惨的是,很多团队直到上线前压测才发现性能瓶颈,甚至等到生产环境报警了,才顺藤摸瓜找到这些“不起眼”的 FirstOrDefault。
而且这类代码往往藏在循环里、服务调用中、或者嵌套逻辑深处,平时根本注意不到,一出事就是大问题。
我们直接上对比代码,一看就懂。
public class MyService
{
public List<EntityBase> Entities { get; } = new();
public EntityBase? GetById(Guid id)
=> Entities.FirstOrDefault(e => e.Id == id);
}
这段代码的问题不是“错”,而是“慢”。随着数据量上升,GetById 会越来越拖后腿。
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;
}
这么一改,好处立马体现:
FirstOrDefault,减少重复代码IReadOnlyDictionary 防止外部误改IReadOnlyDictionary 更安全如果你不希望别人偷偷改你的字典,可以这么写:
private readonly IReadOnlyDictionary<Guid, EntityBase> _entityLookup;
构造函数里初始化完,这个字典就“只读”了,谁也不能增删改。既安全,又让调用方清楚:这玩意儿你不该动。
当然,咱们也不能“一招鲜吃遍天”。Dictionary 虽好,但也不是万能的。以下几种情况,你得慎重考虑:
list[0] 这种索引访问所以一句话:用对场景,才是高手。
光说理论不够直观,咱们来点实打实的测试。
// 准备数据
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 万次查找总耗时 |
|---|---|
List + FirstOrDefault | 6,200ms 以上 |
Dictionary + TryGetValue | 20ms ✅ |
特性 | List<T> | Dictionary<TKey, TValue> |
|---|---|---|
查找性能 | ❌ O(n),数据越多越慢 | ✅ O(1),基本不随数据量增长 |
是否支持键值映射 | ❌ 只能遍历 | ✅ 天生为键值设计 |
是否允许重复元素 | ✅ 可以有重复 ID | ❌ 键必须唯一 |
是否有序 | ✅ 保持插入顺序 | ❌ 不保证顺序(除非用 OrderedDictionary) |
是否支持索引访问 | ✅ list[0] 没问题 | ❌ 不支持 |
适用场景 | 小数据、有序、频繁遍历 | 高频查找、大量唯一 ID 映射 |
一句话总结:
当你看到代码里出现 FirstOrDefault(e => e.Id == id),先别急着提交,问自己一句:
👉 这个查找会频繁执行吗?
👉 数据量会不会越来越大?
👉 能不能一开始就用 Dictionary 预处理?
有时候,就是这么一个小小的改动,能避免未来线上服务“卡到爆”、“查不动”、“QPS 直线下降”。
我们总说:“合适的工具做合适的事。”
List<T> 没毛病,但它不是为“快速查找”设计的。
当你需要根据唯一 ID(比如 Guid)频繁查找对象时,**Dictionary<Guid, T> 才是更高效、更现代、更可维护的选择**。
别等到系统慢得像蜗牛了才去优化。 从写第一行代码开始,就用正确的思路,写出高性能、易维护的程序。
这才是真正“写代码”的样子。
★🔗 相关阅读
如果你觉得这篇文章有帮助,不妨转发给团队里的小伙伴,一起告别“遍历地狱”!