我在 C# 里写了个 LINQ 查询赋给一个变量,以为它已经是查询结果了,结果那个耗时的过滤被反复执行了好多次,还有一次枚举时拿到的数据跟我预期的完全不一样:一次没搞懂 LINQ 延迟执行的深度复盘

我有段代码从数据源筛元素,判断条件是个耗时操作,写成 var result = source.Where(x => SlowCheck(x))。后面用了好几次:result.Count() 看数量、foreach 遍历、result.Any() 判断。我想当然以为 result 就是算好的结果集、几次用的是同一份数据。可接口慢得反常,加日志才傻眼:那个耗时的 SlowCheck 被执行了好几遍——Count 一遍、foreach 又一遍、Any 再一遍。更诡异的是另一处:定义查询后改了 source,foreach 时竟拿到了修改后的数据。复盘才搞懂 LINQ 延迟执行:Where/Select 返回的不是结果而是查询定义,当时根本没执行;真正执行推迟到枚举(foreach/Count/Any/ToList)那一刻,每枚举一次就把整个查询从头执行一遍、且读枚举时刻的数据源。根因是把定义查询误当成已得结果,导致重复执行和读到变化后的数据。这篇复盘从故障现场讲到延迟执行与立即执行的区别、哪些操作触发枚举、两个反直觉后果,再到多次复用就 ToList 物化、要下推数据库就保持延迟、用 First/Take 提前终止的完整正解,以及其他延迟执行相关坑(using 释放后枚举/投影里有副作用/Count 后再遍历),和描述定义不等于已执行的结果、惰性的东西被触发时才发生且每次都重新发生、要搞清何时几次依据什么求值的认知。

我在 C# 里写了个 LINQ 查询赋给一个变量,以为它已经是查询结果了,结果那个耗时的过滤被反复执行了好多次,还有一次枚举时拿到的数据跟我预期的完全不一样:一次没搞懂 LINQ 延迟执行、把"定义查询"当成"已得结果"的深度复盘

那个慢得离谱的接口,让我盯着 LINQ 查询看了半天才回过味来。我有一段代码,从数据源里筛出符合条件的元素,其中的判断条件是个挺耗时的操作(要调一个慢方法)。我写成了 var result = source.Where(x => SlowCheck(x));,然后后面用了好几次:先 result.Count() 看有多少个,再 foreach 遍历处理,中间还 result.Any() 判断了一下。我想当然地以为:result 就是筛选后的结果集,后面这几次用的都是同一份算好的数据。可接口慢得反常,我加日志一看,傻眼了:那个耗时的 SlowCheck,被执行了好几遍——Count() 时跑一遍、foreach 时又跑一遍、Any() 时再跑一遍。更诡异的是另一处:我定义查询后,中途修改了一下 source,结果 foreach 遍历时,拿到的竟是修改后的数据,跟我定义查询那一刻的预期完全不符。复盘 LINQ 的执行机制,我才彻底搞懂,后背发凉:问题出在我没理解 LINQ 的"延迟执行(deferred execution)"。WhereSelect 这类 LINQ 操作符,返回的不是"查询结果",而是一个"查询的定义(IEnumerable,描述了"怎么查")"——它当时根本没执行;真正的执行,被推迟到"你去枚举它(foreach、Count、Any、ToList 等)"的那一刻;所以我每用一次 result(Count、foreach、Any),就把整个查询(包括那个慢 SlowCheck)从头完整执行了一遍——查询定义可以复用,但每次枚举都是一次全新的执行;而那个"数据变了"的诡异现象,也是同理:查询在枚举时才执行、读取的是"枚举那一刻"的数据源,我中途改了 source,枚举时自然读到的是新数据。根本原因是:LINQ 的 Where/Select 等是延迟执行的,返回的是"查询定义"而非"结果",每次枚举都重新执行整个查询、且读取枚举时刻的数据源;而我把"定义查询"误当成了"已得到结果",导致重复执行和读到变化后的数据。问题的根,是误解了 LINQ 延迟执行——把返回查询定义的 Where 当成了已算好的结果,既造成耗时操作被多次枚举重复执行,又导致枚举时读到了已改变的数据源。这篇就把这次"LINQ 延迟执行"的坑,从头到尾复盘一遍。

故障现场:一个查询,被执行了好多遍

问题在于把延迟执行的查询定义当成了已物化的结果:

// 我以为 result 是"算好的结果", 其实它只是"查询的定义", 每次枚举都重新执行:
var result = source.Where(x => SlowCheck(x));   // ← 这行根本没执行SlowCheck! 只是定义了查询

int count = result.Count();      // 第1次枚举 → 完整执行一遍 Where + SlowCheck
if (result.Any())                // 第2次枚举 → 又完整执行一遍!
{
    foreach (var x in result)    // 第3次枚举 → 再完整执行一遍!
        Process(x);
}
// → SlowCheck 被执行了 3×N 次! 我以为只算一次, 实际算了三遍 → 接口慢得离谱。

/*
另一个诡异现象(枚举时读的是"当时"的数据源):
  var query = source.Where(x => x.Active);   // 定义查询(没执行)
  source.Add(new Item { Active = true });    // 中途改了数据源
  foreach (var x in query) {...}             // 枚举时才执行 → 读到了"加了新元素后"的source!
  // 我以为query是"定义那一刻的快照", 其实它每次枚举都重新读当前的source。

★ LINQ 延迟执行(deferred execution)的本质:
  - Where/Select/OrderBy/Take... 这些操作符, 返回的是 IEnumerable(一个"查询的定义/配方"),
    它们"描述了怎么查", 但当时并不执行;
  - 真正执行发生在"枚举"时: foreach、Count()、Any()、First()、ToList()、ToArray()、Sum()...
    这些会"触发枚举"的操作, 才真正跑查询;
  - 每触发一次枚举 = 把整个查询链从头执行一遍(读数据源 + 跑所有Where/Select的委托);
  - 所以: ① 多次枚举 = 多次执行(性能/重复副作用); ② 读的是枚举那一刻的数据源(不是定义时)。

  对比"立即执行": ToList()/ToArray()/Count()/Sum()/First() 等返回"具体值/集合"的, 会立即枚举一次;
  其中 ToList()/ToArray() 把结果"物化"成一个真实集合, 之后用这个集合就不再重复执行查询了。
*/

看着日志里 SlowCheck 被打印了三倍的次数,我又懊恼又恍然:"我一直以为 var result = ...Where(...) 这行就把结果算出来了、存进 result 了,谁知道它只是'记下了怎么算',每次用都重算一遍!"这个坑最隐蔽的地方在于:不报错,结果通常也是对的(每次重算结果一样),只是悄悄地多算了好多遍——所以你很难发现,只会觉得"怎么这么慢";而"读到变化后的数据"那种问题更隐蔽,只在"定义查询和枚举之间数据源恰好变了"时才出现,极难复现下面就来拆解,LINQ 延迟执行到底该怎么用。

第一件事:搞懂延迟执行与立即执行

我顺着这次事故,把 LINQ 的执行机制彻底理清了。

LINQ 的延迟执行 vs 立即执行 到底怎么回事?

【核心: Where/Select等返回"查询定义"(IEnumerable)、延迟到枚举时才执行; 每次枚举都重新执行整个
   查询、读枚举那一刻的数据源; 要复用结果就用ToList()/ToArray()物化一次, 避免重复执行】

1. 延迟执行(deferred): 大部分返回 IEnumerable 的操作符
   - Where、Select、OrderBy、Take、Skip、Distinct、Concat... 都是延迟的;
   - 调用它们只是"构建查询的定义/管道", 不执行;
   - 真正执行 = 被"枚举"时。

2. 触发执行(枚举)的操作:
   - foreach 遍历;
   - 返回单值的: Count()、Any()、First()、Single()、Sum()、Max()、Average()...;
   - 物化集合的: ToList()、ToArray()、ToDictionary()、ToHashSet();
   - 这些一调用, 就会把查询从头跑一遍。

3. 延迟执行的两个"反直觉"后果(我都踩了):
   ① 多次枚举 = 多次完整执行: 同一个查询变量被Count/foreach/Any各用一次 = 跑三遍;
      若查询里有慢操作/查数据库/有副作用, 就是三倍代价/三次副作用;
   ② 读"枚举时刻"的数据源: 定义查询后数据源变了, 枚举时读到的是变化后的(不是定义时的快照)。

4. 解决: 需要复用结果就"物化"一次
   - var list = source.Where(...).ToList();  // 立即执行一次, 结果存进真实List;
   - 之后 list.Count、foreach list、list.Any() 都用这个List, 不再重复执行查询;
   - 物化后也"冻结"了数据(快照), 不再随源变化。

5. 但延迟执行不是坏事(别一律ToList):
   - 它能"组合查询不立即执行"(按需拼接), 对数据库(EF/IQueryable)能把多个Where合成一条SQL、
     只查需要的列和行(Take), 非常高效; 过早ToList反而会拉全量到内存;
   - 它能处理无限序列、惰性流式处理(只算用到的部分, 配合First/Take提前终止)。
   - 原则: "只枚举一次、且希望下推到数据库/惰性"时, 保持延迟; "要多次复用结果/数据源会变/
     就地脱离上下文"时, ToList物化。

一句话: LINQ的Where/Select等延迟执行、返回查询定义、每次枚举都重新执行整个查询并读当时的数据源;
   要多次复用结果就ToList/ToArray物化一次, 但别盲目物化(延迟执行对数据库下推和惰性流式很有价值)。

这套认知,是整个坑的根。延迟执行:Where/Select/OrderBy/Take 等返回 IEnumerable 的操作符都是延迟的,调用只构建查询定义、不执行触发执行的操作:foreach、Count/Any/First/Sum 等返回单值的、ToList/ToArray 等物化的,一调用就把查询从头跑一遍两个反直觉后果:①多次枚举=多次完整执行(有慢操作/副作用就翻倍);②读枚举时刻的数据源(不是定义时的快照)解决:需要复用结果就 ToList() 物化一次,之后都用这个 List、不再重复执行、也冻结了数据但别一律物化:延迟执行对 EF/IQueryable 的 SQL 下推、惰性流式、无限序列很有价值,过早 ToList 反而拉全量到内存一句话:LINQ 的 Where/Select 等延迟执行、返回查询定义、每次枚举都重新执行整个查询并读当时的数据源;要多次复用结果就 ToList/ToArray 物化一次,但别盲目物化。

第二件事:正解——该物化就 ToList,该延迟就保持延迟

知道了延迟执行,正解就清楚了:按"用几次、要不要下推、数据会不会变"决定物化还是延迟。

// 正解1: 要多次复用结果 → ToList() 物化一次(本次该做的)
var result = source.Where(x => SlowCheck(x)).ToList();   // ← 立即执行一次, 存进真实List
int count = result.Count;          // 直接读List的Count, 不再执行查询
if (result.Any())                  // 用List, 不再执行
{
    foreach (var x in result)      // 遍历List, 不再执行
        Process(x);
}
// → SlowCheck 只执行 N 次(物化那一次), 接口恢复正常。

// 正解2: 需要"定义查询那一刻的数据快照" → 也用 ToList() 冻结
var snapshot = source.Where(x => x.Active).ToList();   // 此刻的数据被定格
source.Add(...);                                        // 之后改source不影响snapshot
foreach (var x in snapshot) {...}                       // 遍历的是定格时的数据

// 正解3: 只枚举一次、且希望下推到数据库 → 保持延迟(别过早ToList)
// EF Core: 保持IQueryable延迟, 让多个条件合成一条高效SQL, 只查需要的
var query = db.Orders.Where(o => o.UserId == uid);     // 还没查库
if (onlyPaid) query = query.Where(o => o.Paid);        // 按需拼接条件(延迟的好处)
var page = query.OrderByDescending(o => o.Time)
                .Skip(0).Take(20)
                .ToList();   // ← 最后才ToList: 此时生成一条带WHERE/ORDER/LIMIT的SQL, 只查20条
// 若一开始就 db.Orders.ToList() 再内存过滤 → 把整表拉到内存, 灾难!

// 正解4: 用 First/Any/Take 提前终止(延迟+惰性的好处)
var firstMatch = source.Where(x => SlowCheck(x)).First();  // 找到第一个就停, 不跑完全部

// 核心: "要多次复用结果 / 要数据快照 / 要脱离上下文(如离开using/DbContext)" → ToList物化;
//   "只枚举一次 / 要下推数据库 / 要惰性提前终止" → 保持延迟。判断依据: 用几次、在哪执行、数据会不会变。

这套正解的关键,是分清"该物化"和"该延迟"两种场景,而不是无脑选一种要多次复用结果:用 ToList()/ToArray() 物化一次,之后都用这个真实集合,避免重复执行(本次我该做的)。要数据快照:ToList() 也能冻结定义那一刻的数据,不随源变化。只枚举一次且要下推数据库:保持 IQueryable 延迟,让多个条件合成一条高效 SQL、只查需要的行列,过早 ToList 会把全表拉进内存。提前终止:用 First/Any/Take 利用惰性,找到就停。判断依据就是:用几次、在哪执行、数据会不会变。

第三件事:其他几个"延迟执行/惰性"相关的坑

顺着 LINQ 延迟执行,我把相关的几个坑也一并理了:

几个延迟执行/惰性相关的坑:

坑1: 闭包捕获循环变量(C#5之前的foreach也有, 类似Go 544/JS闭包):
   旧版本里在循环内构建查询、捕获循环变量, 延迟到枚举时变量已是最后的值;
   正解: 循环内用局部副本; 现代C#的foreach变量已是每轮独立, 但for循环仍需注意。

坑2: 在 using/DbContext 释放后才枚举 → 报错(连接已关闭):
   var q = db.Orders.Where(...); // 在using内定义
   // using结束, DbContext释放
   foreach(var o in q) {...}      // ✗ 枚举时才执行 → 上下文已释放, 抛异常!
   正解: 在上下文存活期内 ToList() 物化, 或别让查询逃出using。

坑3: 多次枚举有副作用的序列 → 副作用执行多次:
   若Select里有副作用(写日志、改状态、发请求), 多次枚举会多次触发, 后果严重;
   正解: 别在LINQ投影里放副作用; 要执行副作用就物化后明确遍历一次。

坑4: 对延迟查询做 Count() 判空再枚举 → 执行了两遍:
   if (q.Count() > 0) foreach(q) // Count一遍 + foreach一遍;
   正解: 物化成List再判Count/遍历; 或用Any()(只需判存在, 比Count高效)。

坑5: IEnumerable 当方法返回值, 调用方多次枚举 → 每次重算:
   返回 IEnumerable 给外部, 你不知道对方会枚举几次; 若内部是昂贵查询, 可能被反复执行;
   正解: 昂贵/有副作用的, 返回前 ToList(); 或在文档明确"延迟执行, 请勿多次枚举"。

共同的根: 延迟执行把"定义"和"执行"分开了——"持有一个查询"不等于"持有结果";
   要清楚"它何时真正执行(枚举时)、执行几次(枚举几次)、执行时的环境(数据源/上下文状态)"。

这些坑看似不同,根却是同一个:延迟执行把"定义一个查询"和"执行这个查询"拆成了两个时刻——"持有一个查询变量"不等于"持有一份结果";它只是"一张随时可以照着做的配方",你每"照做(枚举)"一次,它就重新做一次,用的还是"照做那一刻"的食材(数据源)认清这个根("定义≠执行,持有查询≠持有结果"),才能预判它何时执行、执行几次、用什么数据执行。

第四件事:延迟 vs 立即——两张对照表

我把常用 LINQ 操作符的执行时机、以及"该延迟还是该物化",整理成对照表,贴在了团队的 C# 规范里:

操作符 执行时机 返回
Where / Select / OrderBy 延迟 IEnumerable(查询定义)
Take / Skip / Distinct / Concat 延迟 IEnumerable(查询定义)
ToList / ToArray / ToDictionary 立即(物化) 真实集合(快照)
Count / Sum / Average / Max 立即(枚举一次) 单个值
First / Single / Any / All 立即(可能提前终止) 单个值/布尔
foreach 遍历 立即(触发枚举)
场景 该怎么做 原因
结果要用多次(Count+遍历) ToList 物化 避免重复执行整个查询
查询含慢操作/副作用 ToList 物化 避免多次枚举多次代价
需要定义时的数据快照 ToList 冻结 不随数据源变化
查询要逃出 using/DbContext 上下文内 ToList 枚举时上下文可能已释放
只枚举一次 + 想下推数据库 保持延迟 合成高效 SQL,只查所需
无限序列 / 想提前终止 保持延迟 + Take/First 惰性,只算用到的部分

这两张表的核心,第一张是记住"返回 IEnumerable 的多半延迟、返回具体值/集合的立即";第二张是用几次、有没有慢操作/副作用、数据会不会变、要不要下推数据库——这几点决定该物化还是该延迟。记住一条:多次复用、有代价、要快照、会脱离上下文 → 物化;只用一次、要下推、要惰性 → 延迟。

第五件事:关于 LINQ 延迟执行的几组容易想当然的认知

这次事故也让我厘清了几组关于 LINQ 的、容易想当然的概念:

直觉以为 实际上
var r = list.Where(...) 已经算出结果了 只是查询定义,还没执行
同一个查询变量用几次都是同一份数据 每次枚举都重新执行整个查询
查询变量是定义那一刻的快照 枚举时才读数据源,读的是当时的
Count() 后再 foreach 没额外开销 各执行一遍,慢查询就是双倍
在 using 里定义查询就安全了 枚举若在 using 外,上下文已释放报错
ToList 总是多余的开销 多次复用时它恰恰省了重复执行
一律 ToList 最保险 过早物化会丢掉下推/惰性,拉全量到内存

这张表里,我栽的是第一行和第二行:把"定义查询的 Where"当成了"已算好的结果",又以为"用几次都是同一份",完全没意识到每次枚举都在重新执行那个慢操作厘清这些,核心是一个意识:LINQ 把"定义查询"和"执行查询"分成了两步;一个查询变量是"怎么查"的描述,不是"查出来的结果";要清楚它何时执行、执行几次、用什么数据执行。

第六件事:写 LINQ 查询时,我现在的自检习惯

现在每当我写一个 LINQ 查询、准备用它,我都会先按这张图问自己:

这张图的精髓,是"它只是定义、枚举才执行;用多次就物化、要下推就延迟、别让查询逃出上下文"先记它还没执行、再问枚举几次(多次就 ToList)、有无慢操作/副作用/下推需求会不会逃出 using这套习惯,让我从"写完 Where 就以为有结果了"变成了"清楚它何时、执行几次、用什么数据执行"——核心始终是:LINQ 的 Where/Select 延迟执行、返回查询定义、每次枚举都重新执行并读当时数据源;多次复用就 ToList 物化、要下推数据库就保持延迟,别把定义查询当成已得结果。

我立下的几条规矩

这场"一个查询被反复执行"的事故,换来了我写 LINQ 时,刻进骨子里的几条铁律:

  1. Where/Select/OrderBy 等返回 IEnumerable 的操作是延迟执行的,只是查询定义,当时不执行。
  2. foreach、Count、Any、First、ToList 等"枚举"操作才触发执行;每枚举一次就完整执行一遍。
  3. 查询读的是"枚举那一刻"的数据源,不是定义时的快照。
  4. 结果要用多次、或查询含慢操作/副作用 → ToList()/ToArray() 物化一次。
  5. 查询会逃出 using/DbContext → 在上下文存活期内物化,别让枚举发生在释放后。
  6. 只枚举一次、要下推数据库或要惰性提前终止 → 保持延迟,别盲目 ToList 拉全量。
  7. 别在 LINQ 投影(Select)里放副作用;判存在用 Any() 而非 Count()>0。

附:几个验证延迟执行的小实验

为了让团队成员直观感受延迟执行,我写了几个一跑就明白的小实验,贴在内部 wiki 上。

// 实验1: 证明 Where 当时不执行(看输出顺序)
var q = new[] { 1, 2, 3 }.Where(x => {
    Console.WriteLine($"检查 {x}");   // 副作用: 打印
    return x > 1;
});
Console.WriteLine("--- 查询已定义, 但上面的'检查'还没打印 ---");
foreach (var x in q) Console.WriteLine($"取到 {x}");
// 输出顺序证明: "查询已定义"先打印, 之后foreach时才打印"检查1/检查2/检查3"
//   → Where在定义时根本没跑, 枚举时才跑。

// 实验2: 证明多次枚举多次执行
int calls = 0;
var q2 = new[] { 1, 2, 3 }.Where(x => { calls++; return true; });
q2.Count();                 // 枚举一遍
q2.ToList();                // 又一遍
foreach (var _ in q2) { }   // 再一遍
Console.WriteLine($"委托被调用了 {calls} 次");   // 输出 9 (=3×3), 不是3!

// 实验3: 证明读的是枚举时的数据源
var list = new List { 1, 2 };
var q3 = list.Where(x => x > 0);
list.Add(3);                          // 定义查询后, 改数据源
Console.WriteLine(q3.Count());        // 输出 3, 不是2 → 枚举时读到了新增的元素

// 实验4: ToList 物化后就冻结了
var q4 = list.Where(x => x > 0).ToList();   // 物化
list.Add(4);                                 // 再改源
Console.WriteLine(q4.Count);                 // 还是3 → 物化后是快照, 不随源变

这几个实验,把"延迟执行"从抽象概念变成了可眼见的输出:实验 1 证明"定义时不执行、枚举才执行";实验 2 证明"枚举几次执行几次"(委托被调 9 次而非 3 次);实验 3、4 证明"延迟查询读枚举时的源、物化后是快照"我发现,对这类"反直觉"的机制,与其反复讲道理,不如让人亲手跑一个"输出会出乎意料"的小实验——那种"咦,怎么是 9 不是 3"的意外,比任何文档都让人记得牢。

写在最后

回头看,这场由"误把 LINQ 查询定义当成结果"引发的、查询被反复执行的事故,真正教给我的,远不止"多次复用要 ToList"这一个技巧。它让我对"'描述一件事怎么做' 和 '这件事真的被做了', 是两个不同的时刻、两件不同的事; 我们却常常一拿到'做法/计划/定义', 就误以为'事情已经办成了'",有了一次刻骨的体会。我栽跟头,是因为我把"持有一个查询(怎么查的描述)"等同于了"持有一份结果(查出来的数据)"——我拿到了一张"菜谱", 就以为我拿到了一盘"";可"菜谱"只是"怎么做这道菜的描述", 我每"照着做(枚举)"一次, 才真正炒出一盘菜, 用的还是"炒那一刻冰箱里的食材(数据源)";我以为看一眼菜谱就吃上了, 结果是每次想吃都得重新炒一遍(重复执行), 而且食材还可能换了(数据变了)这让我领悟到一个关于"描述与实现、延迟与求值"的深刻认知:"定义/描述/计划/承诺/配方"(说明"该怎么做、要做什么")和"执行/实现/结果"(事情真正发生、产生了效果),是两个必须区分的阶段——"拥有一份精确的描述" 绝不等于 "这件事已经被完成了";很多东西是"惰性/延迟"的: 它只在"被真正需要、被触发"的那一刻才发生, 而且每触发一次就发生一次、用的是触发那一刻的环境——计划要执行了才算数、承诺要兑现了才作数、配置要被加载了才生效;分不清"描述"和"已发生", 就会误判事情的状态(以为办好了其实没动)、误判代价(以为算一次其实算多次)、误判依据(以为用的旧环境其实是新的)这给了我一种看待"计划与执行"的清醒:面对任何"看起来像结果、其实只是描述/定义/计划"的东西时,要清醒地追问"它真的执行了吗?会在何时执行?执行几次?执行时依据的是什么状态?"——不把"我定义好了/计划好了/它返回了一个对象"当成"事情已经完成、结果已经固定";"清醒区分'描述一件事'和'这件事已发生', 并搞清惰性的东西何时、几次、依据什么被真正求值",是避免'把配方当成菜、把计划当成结果'式误判的关键认清描述定义不等于已执行的结果、惰性的东西被触发时才发生且每次都重新发生、要搞清何时几次依据什么求值——这,是我用一次 LINQ 延迟执行的事故,换来的、关于 C#、也关于如何区分描述与实现的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次把一个 LINQ 查询要用上好几次之前,先顺手 .ToList() 一下,那我对着那个被执行了三遍的 SlowCheck 日志发愣的这半天,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我用消息队列解耦订单处理,以为一条消息只会被消费一次,结果某条扣库存的消息被重复消费了两次,库存莫名其妙被多扣了一份:一次没为消息重复投递做幂等、误把至少一次当成恰好一次的深度复盘

2026-6-3 0:07:47

技术教程

我在 TypeScript 里用 as 把接口返回的数据断言成了我定义的类型,以为这下类型安全了可以放心用,结果运行时那个字段是 undefined 直接报错,因为 as 根本不做任何检查:一次滥用类型断言的深度复盘

2026-6-3 0:19:48

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索