
“常见的.NET面试题”这个话题我前后经历了两个身份去接触。先是自己找工作那阵子对着网上的题目清单一条条背背到后来发现题和题之间其实是一张网后来参与技术面坐在对面看候选人答题又验证了一件事——绝大多数“基础题”并不是想听你背口诀而是想看你能不能把“是什么”“为什么”“会有什么影响”串起来讲。比如同样问值类型和引用类型只答“结构体是值类型类是引用类型”和能从内存分配讲到装箱拆箱性能影响完全是两个档次的回答。这篇笔记把这几年高频出现的.NET面试题按知识域拆开覆盖C#语言基础、集合与LINQ、异步编程、依赖注入、垃圾回收和EF Core数据访问。每个题目我会写明考查点、回答思路还有实际答题时最容易丢分的细节。适合正在准备跳槽的.NET开发也适合刚入行想系统梳理C#和ASP.NET Core知识的人。基础好的朋友可以直接跳到薄弱章节查漏补缺不需要从头到尾读。1. 面试题背后的知识地图面试官到底在考什么1.1 三类出题思路决定了你该怎么准备我把常见的.NET面试题先做个粗分类不是为了让你按清单背而是让你看到面试官出题时的知识树长什么样。第一类是C#语言与CLR基础。值类型与引用类型、装箱拆箱、string与StringBuilder、委托与事件、异常处理、GC与内存管理都属于这一类。这类题的特点是“看着简单”但每道题都能一路追问到运行时层面比如问到GC就会牵扯代龄、根对象、托管堆。第二类是框架设计与依赖注入。ASP.NET Core的中间件管道、服务生命周期、配置系统、选项模式这些题考查的是你对框架设计思路的理解。生命周期是这里的最高频考点几乎没有一场面试能绕开。第三类是数据访问与异步编程。EF Core的查询执行、SQL注入防护、乐观并发、连接池加上async/await、Task、线程安全和并发集合。这类题更偏向工程实践面试官喜欢给一个具体场景看你怎么分析和选型。搞清楚这个分类你再去看网上的面试题清单就不会慌。很多题看似无关其实考的是同一个底层机制。比如“为什么ASP.NET Core依赖注入里Scoped服务不能注入到Singleton服务”和“为什么事件没退订会导致内存泄漏”本质都是对象生命周期与引用关系的理解。1.2 回答问题的通用框架先给结论再讲原理最后落到工程我参与面试时最直观的感受是候选人答得差往往不是不会而是不会组织回答。被问到任何一道题我建议都用“一句话结论 两分钟原理 一句工程影响”的结构来答。举个例子被问“struct和class有什么区别”。结论先说struct是值类型class是引用类型。原理再展开值类型变量直接持有数据赋值时复制整个数据引用类型变量持有堆上对象的引用赋值时只复制引用。工程影响补充所以大量小对象且频繁复制的场景选struct可以少分配堆内存但不可轻易给struct加可变字段因为容易出现“改了副本原数据没变”的困惑。这个回答框架的好处是面试官可以按你的节奏继续追问。你先给结论他判断你基础扎实你再讲原理他判断你理解深度最后落到工程他判断你有实践意识。最怕的是候选人一上来就讲底层讲半天没落到代码上或者只背了一两句定义追问两句就卡住。2. 值类型、引用类型与装箱拆箱先把地基打牢2.1 栈和堆不是这道题的全部在.NET面试中值类型和引用类型基本是必问的第一道题因为它能引出非常多的子问题。网上最常见的回答是“值类型分配在栈上引用类型分配在堆上”。这个说法对局部变量基本成立但不完整。比如一个类里有一个int字段这个int是值类型但它作为类对象的一部分整体分配在托管堆上并不会单独跑到栈上去。更准确的理解是值类型变量直接持有数据引用类型变量持有指向托管堆对象的一个引用相当于地址。栈更适合用来存方法调用过程中生命周期明确的小数据因为LIFO的结构让清理非常高效而堆上的对象生命周期不固定才需要GC来统一管理。面试中如果能把“值类型并不总是分配在栈上”这一点主动补充出来通常会是明显的加分项。这说明你不是背了一句结论而是真的想清楚了内存布局。还可以顺手解释一下“栈是线程私有的堆是进程内共享的”把线程和内存的关系也带出来。2.2 装箱拆箱考察的是“是否理解对象模型”装箱拆箱是值类型话题里最经典的延伸题因为它在.NET早期版本里是真实的性能陷阱。看下面这段代码int num 42; object obj num; // 装箱堆上分配对象把42复制进去 int back (int)obj; // 拆箱先检查类型再复制回栈装箱发生在值类型被转换为object或接口类型时CLR会在托管堆分配一个新对象把值类型字段复制进去并在托管堆上留下一个对象头。拆箱则是反向操作先把object中的值提取出来复制给值类型变量过程中还会做类型检查如果类型不匹配会抛InvalidOperationException。实际操作中装箱最常见的发生场景是使用非泛型集合、调用接受object参数的方法以及把值类型赋值给interface类型的变量。比如老代码里的ArrayList.Add(int)每次Add都装箱一次换成List 之后完全避免装箱。如果面试官追问“什么时候装箱影响明显”可以答“大量循环内的高频调用”比如把千万级整数逐个装箱会产生千万个短命对象直接抬高GC压力。2.3 字符串相关题目最容易忽略的细节string本身是引用类型但有着值类型一样的“不可变”特性这一点经常被混淆。面试题“string和StringBuilder的区别”几乎必问多数人能答出“string不可变拼接会产生新对象StringBuilder内部维护可变字符缓冲区”但再往下追问就容易露怯。要注意的是StringBuilder并不是拼接字符串永远更快。短字符串、少数几次拼接直接用字符串插值或string.Concat就行创建一个新对象并不是什么大问题。StringBuilder的优势体现在循环内多次追加比如循环1万次拼SQL或拼HTML时能避免创建大量中间字符串。另一个容易被忽略的考点是字符串驻留池。CLR会复用编译期相同的字符串字面量所以两个内容相同的字面量可能指向同一个对象。但运行时动态拼接出来的字符串不保证驻留所以比较字符串永远应该用Equals或string的重载是按值比较不要用ReferenceEquals去赌驻留行为。3. 集合框架与LINQ从会用到讲出底层机制3.1 List与Dictionary的底层结构与扩容集合类的面试题通常不会直接让你写API而是让你解释“为什么List插入快Dictionary查找快”。List 内部是一个数组通过索引读取是O(1)插入到末尾通常也是O(1)但插入到中间需要移动后续元素。数组容量不够时List会扩容每次扩容为当前容量的两倍并把老数组的元素复制到新数组这是一个相对昂贵的操作。所以“预见大量元素时给List设置初始容量”是既实用又能体现经验的回答。DictionaryTKey, TValue底层是哈希表结构。它先用键的哈希码定位到某个桶再把键值对放进桶里如果多个键哈希到同一个桶就形成冲突链。查找时先算哈希码找桶再在桶内比较键平均复杂度是O(1)。因此键类型需要实现合理的GetHashCode和Equals如果哈希码分布差Dictionary会退化成类似链表查询的性能。这个问题面试官喜欢给场景一个自定义类做字典键时要注意什么你要答出重写GetHashCode和Equals、保持哈希码稳定、避免用可变更字段当键。3.2 foreach循环的枚举器机制另一个高频基础题是“foreach遍历时为什么不能修改集合”。这要从foreach的实现原理讲起。foreach本质上是编译器生成的语法糖它会获取集合的枚举器然后不断调用MoveNext去取Current。数组有特殊优化编译器会直接生成索引循环避免分配枚举器对象而List这类集合枚举器内部会记录集合的版本号每次修改集合Add、Remove、Clear都会让版本号变化下一次MoveNext发现版本不一致就抛出InvalidOperationException。这给了我们一个反常识的结论foreach并不比for慢多少数组的foreach甚至被编译器优化成和for一样的索引循环但如果你在循环里需要修改集合应该用for从后往前删或者改用List的RemoveAll、临时列表收集后再批量处理。面试中如果能顺手答出“版本号机制”这个细节通常会让面试官觉得你读过实现源码。3.3 LINQ延迟执行面试官最爱设的坑LINQ是.NET面试里辨识度很高的考点。很多候选人会写LINQ但没理解“延迟执行”和“立即执行”的边界。面试官常拿下面这个代码片段来问输出是什么var list new Listint { 1, 2, 3 }; var query list.Where(n n 1); list.Add(4); foreach (var n in query) { Console.WriteLine(n); }这段代码会输出2、3、4。原因是Where属于延迟执行它不会在定义查询时就过滤数据而是等到foreach真正枚举时才去源集合里取元素。所以list后来又加了4枚举时4也会被取出来。相反ToList、ToArray、Count、First等操作会立即执行查询得到结果后源集合再怎么变都不影响。这个知识点在工程上有非常实际的意义。一个查询如果被两次foreach枚举它就会执行两次如果过滤条件里引用了随机值或当前时间两次结果都可能不同。延迟执行本身是LINQ的优势因为它可以组合多个操作只遍历一次源序列但不注意缓存结果就会写出“看起来对线上问题反复出现”的代码。推荐的做法是确定要复用查询结果时先ToList()固化。3.4 IEnumerable与IQueryable该何时区分这类题在EF Core候选人中很常问。IEnumerable与IQueryable最核心的差别是IEnumerable在内存中迭代集合元素操作是委托在本地执行IQueryable保存的是表达式树可以由查询提供程序比如EF Core的翻译器转换成SQL在数据库端执行。举个具体场景。你在EF Core里写var query db.Users.Where(u u.Age 18); var list query.AsEnumerable().Where(u u.City Shanghai).ToList();第一个Where在数据库端翻译成SQL过滤第二个Where因为AsEnumerable已经切到内存会把所有18岁以上的用户全量加载到内存再在内存里过滤城市。数据量小的时候察觉不到表几百万行的时候效果天差地别。所以回答这道题时一定要落到“查询下推”和“内存过滤”的边界上最好再补一句能用IQueryable链式组合查询时不要随手把它转成IEnumerable。4. 异步编程与线程安全敢答还要答得对4.1 从Thread到Task再到async/await的演进逻辑异步编程出现在.NET面试里的概率非常高而且通常会从“Thread和Task有什么区别”问起。Thread是底层的手工创建线程创建和销毁成本很高线程数量也不容易控制ThreadPool线程池能复用线程减少创建销毁开销但用起来还是偏底层Task是对异步操作的更高层抽象表示一个可能还没完成的操作配合async/await使用写起来就像同步代码。面试中最容易出错的点是那句“async/await会创建新线程”。这个说法是错的。async/await本身不创建线程它利用编译器生成的状态机在await等待期间让出当前线程线程可以回去线程池做别的事等异步操作完成后再继续执行方法剩下的代码。真正跑在ThreadPool上的可能是Task内部操作比如Task.Run确实会调度到线程池但await本身只是挂起和恢复的机制。我建议准备这道题时把状态机这个词讲出来不用太深入实现细节但要能让面试官知道async方法会被编译成状态机结构await会拆分成若干步骤在等待完成时保存状态并返回值完成后通过回调控件继续执行。4.2 await后面的线程去哪里了同步上下文“await之后后面的代码跑在哪个线程上”是比“会不会创建新线程”更进阶的考点能问到这里说明面试官想看你是否真的理解上下文。这个问题涉及SynchronizationContext。在WinForms、WPF这些GUI程序里UI线程有同步上下文await后面的代码默认会尝试回到UI线程执行方便你直接更新控件。在ASP.NET Core服务端请求处理并不依赖特定同步上下文await恢复后一般在线程池线程上继续。这就是为什么GUI应用里如果用了async/await控件更新通常很自然而控制台或服务端程序里线程可能会变。有个细节值得补充ASP.NET Core里没有SynchronizationContext但这不代表变量共享就安全跨线程访问共享状态仍然要加锁或使用并发集合。同步上下文只解决“从哪里恢复”不解决“数据竞争”。4.3 阻塞等待与死锁的典型场景死锁是异步面试题里的必问陷阱。最经典的写法是在同步代码里调用异步方法然后用.Result或.Wait()阻塞等待。在GUI应用里这很容易死锁UI线程被阻塞异步操作的后续要回到UI线程但UI线程还在阻塞等待两边互相等。public string GetValue() { return GetValueAsync().Result; // 在UI上下文里可能死锁 } private async Taskstring GetValueAsync() { await Task.Delay(100); return ok; }控制台应用没有同步上下文这个写法通常不会死锁但这不是可以依赖的行为。正确的做法是从调用链顶层一路async/await到最低层避免同步阻塞异步方法。我遇到过不少线上事故最终根因都是“图省事用.Result取的”所以现在代码评审里看到.Result和.Wait()都会重点核查。另一个异步相关的坑是async void。方法声明为async void后异常无法被调用方的try/catch捕获会直接传播到同步上下文在GUI程序里可能导致进程崩溃。只有事件处理器这类“必须返回void”的场景才用async void其他情况一律返回Task或Task 。4.4 并发场景下的集合与锁除了解异步原理线程安全也是个高频场景题。一般会问“List线程安全吗多个线程同时Add有问题吗”直接回答List不是线程安全的并发读写会导致数据损坏。然后给出方案短临界区用lock计数器用Interlocked读多写少的共享数据用ReaderWriterLockSlim或直接考虑不可变数据需要线程安全的字典用ConcurrentDictionary。ConcurrentDictionary用锁分段或细粒度锁实现并发安全TryAdd、GetOrAdd、AddOrUpdate这些原子方法很实用。另一个容易踩的坑是“锁的对象”一般只锁私有只读对象不要锁定字符串字面量或this因为无法控制谁还锁了同一个对象。如果多个不同的类互相锁定还可能引发死锁加锁顺序要固定。5. 依赖注入与生命周期现代.NET应用的骨架问题5.1 为什么必问生命周期.NET面试中对依赖注入生命周期的热衷超出很多候选人的预期。一来ASP.NET Core框架本身强烈依赖DI容器二来生命周期直接关联对象复用、线程安全和内存占用属于架构层面的基础认知。依赖注入的核心是“把对象的创建和管理交给容器”。你注册服务的时候指定生命周期容器负责在解析时创建实例、在合适时机释放。好处是类与类之间的依赖关系由容器装配代码可测性也更好。但要真的用好DI容器必须知道每种生命周期会创建多少个实例、实例会存活多久、是否多线程共享。5.2 Singleton、Scoped、Transient的核心差异生命周期实例创建方式典型使用场景需要警惕的点Singleton第一个请求时创建全局复用同一个实例配置对象、无状态服务、日志器内部不能保存请求级状态注意线程安全Scoped每个作用域通常每个HTTP请求内一个实例EF Core的DbContext、当前用户上下文不能注入到更长的生命周期服务中Transient每次解析都创建新实例用完即丢轻量无状态服务如果持有大对象或非托管资源创建释放频繁最常考的是“为什么DbContext默认用Scoped”。因为一个请求内的DB上下文应该保持统一多个服务在同一个请求里共享同一个DbContext可以保证同一上下文跟踪实体的一致性同时又不会像Singleton那样跨请求共享导致并发操作同一上下文。把Scoped理解成“一个请求内的小单例”会容易很多。5.3 Captive Dependency与对象释放的坑生命周期考点里最值钱的是Captive Dependency也就是“长生命周期服务捕获短生命周期服务”。典型场景Singleton服务里注入了一个Scoped服务。容器会把Scoped服务捕获到Singleton实例里让它的存活期从“一个请求”变成“整个应用”。这可能在多请求并发时造成状态错乱比如一个用户的数据被另一个用户读取到。回答类似题目时要主动指出Singleton的实例是在第一次解析时创建的它捕获Scoped依赖后这个Scoped实例就变相成了单例。正确的做法是把Scoped服务改成通过工厂方式按需解析或者调整服务的注册生命周期。关于对象释放还要知道一套规则容器创建的实例容器负责释放你自己new出来的对象容器不会管。所以在构造函数里new依赖对象、寄放到容器的做法会让释放链路变得混乱。常见的ObjectDisposedException往往就是Scoped或Singleton对象在请求结束后还被子线程持有使用。这个问题排查时先看对象是从容器解析的还是手工new的再看生命周期匹配不匹配。6. 托管内存与GC高级职位的分水岭6.1 分代回收与代龄分配GC相关题目在中高级岗位面试里出现频率很高因为它直接反映你对程序和运行时交互的理解。CLR的垃圾回收器不是简单的引用计数而是基于对象可达性分析从根对象出发沿着引用遍历能访问到的对象视为存活遍历不到的标记为垃圾。根对象包括静态字段、线程栈上的局部变量、CPU寄存器里的引用等。GC把托管堆分成0代、1代、2代。新对象分配在0代0代满了会触发回收存活的对象晋升到1代1代再满则回收0代和1代幸存的晋升到2代2代回收也就是Full GC。此外还有一个大对象堆通常85KB以上的对象直接进大对象堆因为移动大对象开销太大一般不被压缩。面试时如果能说出“短命对象尽量在0代被回收是GC高效的基石”就是一段加分回答。这也能引导出实践建议不要太频繁创建大对象因为大对象堆回收成本高尽量让临时对象存活时间短。6.2 常见内存泄漏的四个意外来源“什么才算内存泄漏”在.NET里容易聊开。严格意义上托管内存会被GC回收没有C那种完全回不来的泄漏但因为引用被错误地持有导致对象永远无法被回收却是真实存在的工程问题。第一类是事件订阅未退订。你订阅了某个事件事件发布者内部持有你的事件处理委托等同于持有了你的对象的引用。如果发布者生命周期长订阅者销毁了却忘了退订订阅者对象就一直在“不可达”的边缘被根引用着无法回收。修正方法是对应取消订阅或者在合适时机用弱事件模式。第二类是静态集合无限增长。静态Dict或List当作缓存用只加不删时间长了必然膨胀。解决思路是设置缓存上限、过期策略或用带过期机制的缓存组件。第三类是非托管资源没有释放。文件句柄、数据库连接、Socket、GDI对象这些不受GC直接管理必须通过Dispose或Close释放否则不仅内存涨还可能出现句柄耗尽。第四类是单例持有大对象或集合。Singleton生命周期很长里面塞了不断增长的集合或者大对象等于给应用埋了一颗内存炸弹。排查工具方面可以提dotnet-dump抓转储、对比两次内存快照、用分析器看对象引用路径。不用输出具体命令细节但说出“找引用路径”这个思路就对了。6.3 IDisposable与using的本质提到释放就绕不开IDisposable。被问“using做了什么”时直接说“using是try/finally的语法糖finally里调用Dispose”是标准答案但可以加点实质Dispose的目的是释放非托管资源比如连接、句柄、事件订阅调用Dispose不等于对象立即被GC回收对象本身仍然等GC来清理。实现IDisposable时还有个常见问题是否要加终结器析构函数。只有持有非托管资源时才需要终结器由GC单独线程调用释放时机不确定不要用来做常规清理。业务代码里多数时候直接用using包住使用资源的代码块就够了不需要自己实现完整的Dispose模式。面试回答这道题我推荐带一句“using的好处是即使中途抛异常Dispose也能被调用”这比单纯解释语法更有工程价值。7. 数据访问与EF Core实操题的常客7.1 参数化查询与SQL注入数据访问题目里SQL注入永远不会缺席因为它是安全题也是体现编码习惯的题。最危险的写法是把用户输入直接拼进SQL字符串var sql SELECT * FROM Users WHERE Name input ;如果input里传入“a OR 11”整个查询条件就被改写后果可想而知。正确的做法是永远使用参数化查询。ADO.NET里用SqlParameterEF Core的普通LINQ查询会自动参数化用FromSqlRaw时自己要传SqlParameter从SqlInterpolated则会把插值当成参数处理。这道题的深层逻辑是“数据和代码分离”。参数化后输入永远被当成数据不会被解释成SQL结构。面试时如果能把这个原则讲出来会比单纯背“不要拼接SQL”更有说服力。7.2 乐观并发与并发冲突处理EF Core场景题里“如何处理并发冲突”出现的频率相当高。EF默认采用乐观并发读取数据时不加锁提交更新时才检查数据是否被别人改过。实现方式通常是用并发令牌字段比如版本号或者更新时间。提交时框架会用原始值做条件更新如果影响行数为0说明数据已经被修改抛出DbUpdateConcurrencyException。回答这道题的技巧是不仅要解释原理还要给处理方案。捕获到并发冲突后常见做法有三个重新读取数据库当前值再尝试合并丢弃本次修改让用户重新编辑或者以最后一次写入为准。工程上一般会提示用户“数据已被其他人修改请刷新后重试”。顺带解释一下为什么不用悲观锁在高并发Web场景下长时间锁表会严重压缩吞吐量且分布式架构下跨进程锁很难协调。乐观并发牺牲一点冲突概率换取无锁读取的高并发这是Web系统中的常见选择。7.3 连接池与资源释放数据库连接的题目还会考连接池。SqlConnection默认开启连接池连接关闭或释放后物理连接并不真正断开而是回到连接池中复用避免每次建立TCP连接的开销。但连接池不是无限的默认最大连接数有上限如果连接一直没关闭池被耗尽时新请求就会超时报“Timeout expired”之类的错误。所以“用完连接一定要释放”不是口号。最稳妥的是用using块或者确保拿到连接后放到try/finally里Dispose。有些开发者喜欢把SqlConnection做成类字段或缓存起来复这在多数场景下都是坏味道。EF Core的DbContext也类似它是Scoped生命周期默认每个请求一个实例不要试图在Singleton服务里长期持有DbContext那是典型的并发和资源问题。还可以提一下查询性能相关的AsNoTracking。只读查询用AsNoTracking可以省掉实体跟踪开销数据量大的时候效果明显但如果后续要修改实体并保存就不要用因为脱离跟踪后你改的东西框架不知道。8. 陷阱题和真实排查实录答错率高但有规律8.1 闭包捕获与for/foreach变量差异闭包是C#面试里一类特别经典的陷阱题题干经常长这样var actions new ListAction(); for (int i 0; i 3; i) { actions.Add(() Console.WriteLine(i)); } foreach (var action in actions) { action(); }输出是什么答案是3、3、3。原因是for循环的循环变量i定义在循环外所有lambda表达式捕获的是同一个变量循环结束后i已经变成3。换成foreach循环C# 5及以上每次迭代会引入一个新的循环变量lambda各自捕获自己的副本输出就是0、1、2。这里要特别注意语言版本差异C# 5之前foreach的行为和for类似但现代.NET环境基本不用考虑。这个陷阱在工作里也能遇到经典场景是把循环变量放进Task或事件处理器里一执行发现全是最后一个值。修复方式就是用局部变量拷贝一份再捕获或者直接用foreach。能把这个题答清楚的人说明对变量作用域和闭包捕获机制是真的理解了。8.2 值类型修改陷阱索引器返回的是副本另一个答错率高的代码题是“为什么改不进去”。看这段struct Person { public string Name; } var people new ListPerson(); people.Add(new Person()); people[0].Name 张三; // 编译错误 CS1612原因是List 的索引器返回的是Person的副本而不是内部存储的引用。你试图修改副本上的Name改完这个副本就被丢弃内部数据没变。编译器直接判定这种操作没有意义所以报错。解决方案是把Person改成class或取出副本改完后再放回去或用数组。这个题对应的工程经验是设计struct时要偏向“小而不可变”像DateTime、Guid、KeyValuePair这样。大型可变的“数据载体”应该用class否则很容易出现“改了半天数据没变”的诡异问题。8.3 解析与异常处理的选择int.Parse、int.TryParse这类题看着基础但特别能反映编码习惯。int.Parse在格式不合法或为null时直接抛FormatException或ArgumentNullExceptionTryParse返回bool不抛异常解析失败时输出参数为默认值。高频解析场景下用Parse撞异常会让异常系统承担额外的开销尤其在一个循环里反复解析脏数据性能会明显变差。更关键的是编码体验用TryParse可以把“能否解析”当作正常业务分支处理而不是依赖异常流来控制流程。这里可以顺手提一句“异常是给真正异常情况准备的不要把异常当作业务判断”这句话在.NET面试里很加分。8.4 一次连接池耗尽的排查流程最后分享一个具有代表性的排查过程。现象是一个内部系统在高并发后频繁出现接口超时数据库本身负载并不高应用日志里能看到连接池报错提示获取连接超时。第一反应是并发量太大连接数不够于是调大Max Pool Size但效果短暂最终还是超时。后来全面排查代码发现某处手工访问数据库的代码只做了打开连接却没有在finally里关闭。连接池里的物理连接一直被占用池很快耗尽。修复方式是把所有SqlConnection的获取和释放统一改成using并给项目加了连接数监控。从那以后同类问题再没出现过。这个案例对面试的启发是遇到连接池问题不要只想到调数字先检查连接是否被可靠释放。面试官喜欢这类经验因为它说明你不是只背原理而是真的踩过坑、有排查思路。9. 高频考点速查表9.1 高频题速查题目核心考点回答要点struct与class区别值类型/引用类型赋值语义、内存位置、装箱影响装箱拆箱性能模型堆对象分配、类型检查、泛型规避string与StringBuilder不可变与缓冲区拼接场景、中间对象数量async/await是否创建线程状态机与上下文不创建线程、同步上下文恢复.Result死锁阻塞与上下文不要同步阻塞异步、一路async向上DI生命周期生命周期差异Singleton/Scoped/Transient及捕获陷阱LINQ延迟执行惰性求值与立即执行Where延迟、ToList立即、重复执行风险IEnumerable与IQueryable表达式树与翻译数据库过滤与内存过滤边界SQL注入防护参数化数据与代码分离原则乐观并发冲突处理并发令牌、DbUpdateConcurrencyExceptionGC分代可达性分析0/1/2代晋升、大对象堆内存泄漏事件引用持有发布者持有订阅者引用、退订9.2 怎么用这份速查表复习速查表不是让你背而是用来做自测。看到一道题先在心里用“结论、原理、工程影响”三段式回答一遍再看表格里的核心考点是不是都讲到了。我自己准备面试时有个习惯每道题都会写一个十行以内的小Demo去验证比如LINQ延迟执行、闭包捕获、TryParse都亲手跑过一遍。这样面试时讲出来的东西有真实手感而不是干巴巴的理论。准备.NET面试说到底是在整理你对语言和框架的理解把零散的经验归拢成一张知识网。把这些常见题背后的原理真正消化掉面试时你会发现很多“新题”其实都是“老题”换了层皮。