☰
.NET面试题全面解析:从C#基础到EF Core的高频考点与实战回答
2026/10/15 2:06:22 网站建设 项目流程

“常见的.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设置初始容量”是既实用又能体现经验的回答。

Dictionary<TKey, 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 List<int> { 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 Task<string> 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 '1'='1”,整个查询条件就被改写,后果可想而知。正确的做法是永远使用参数化查询。ADO.NET里用SqlParameter,EF 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 List<Action>(); 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 List<Person>(); 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或ArgumentNullException;TryParse返回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注入防护参数化数据与代码分离原则
乐观并发冲突处理并发令牌、DbUpdateConcurrencyException
GC分代可达性分析0/1/2代晋升、大对象堆
内存泄漏事件引用持有发布者持有订阅者引用、退订

9.2 怎么用这份速查表复习

速查表不是让你背,而是用来做自测。看到一道题,先在心里用“结论、原理、工程影响”三段式回答一遍,再看表格里的核心考点是不是都讲到了。我自己准备面试时有个习惯,每道题都会写一个十行以内的小Demo去验证,比如LINQ延迟执行、闭包捕获、TryParse,都亲手跑过一遍。这样面试时讲出来的东西有真实手感,而不是干巴巴的理论。

准备.NET面试,说到底是在整理你对语言和框架的理解,把零散的经验归拢成一张知识网。把这些常见题背后的原理真正消化掉,面试时你会发现,很多“新题”其实都是“老题”换了层皮。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询