
简介这份文档面向准备C#技术面试的开发者与应届求职者聚焦语言基础与核心机制的高频考点帮助读者在短时间内梳理易混淆概念、建立答题思路。内容以问答形式展开覆盖变量初始化中string声明、空串与null的内存差异string与StringBuilder的不可变性与适用场景GC回收原理const与readonly的赋值规则接口与抽象类的选择依据struct与class的值/引用类型区别try-finally中return的执行顺序以及数组随机去重、foreach遍历实现等编程题。资源包共1个doc文件约316KB篇幅紧凑、条目清晰适合作为面试前的速查清单与知识点自测材料。已有372人学习可帮助读者对照自身理解查漏补缺强化对内存管理、多态设计与异常处理等核心概念的掌握。1. 从一道“送命题”说起C# 面试题到底是什么如果你搜过 C# 面试题大概率见过这样一道题string str;、string str ;、string str null;三者有什么区别很多人第一眼觉得“不都是空字符串吗”结果一开口就翻车。这道题之所以经典是因为它表面在问字符串实际在问内存、栈与堆、值类型与引用类型这些 C# 的底层认知。C# 面试题不是背答案的题库而是一套用具体问题检验你是否真正理解语言机制的“认知体检表”。搞懂它你收获的不是几十条标准答案而是看代码时能一眼看穿“它到底在内存里干了什么”的能力。这份资料把 60 多个高频问题按概念归类正好适合拿来建立这套认知。2. 为什么 C# 面试题总爱问“内存”栈、堆与字符串的三角关系2.1 栈和堆一个像书桌一个像仓库理解 C# 面试题里一大半问题的钥匙是先把栈和堆分清楚。通俗地说栈像你手边的书桌空间小、拿取快放的是“当前正在用的东西”——局部变量、方法参数、对象的引用地址堆像后面的仓库空间大、分配慢放的是“对象本体”。栈是编译期间就分配好的大小固定堆是程序运行期间动态分配的按需增长。所以当你写string str abc;时栈上那个str只是书桌上的一张便签写着仓库里“abc”这个箱子的位置真正的字符串内容躺在堆里。这个“便签与箱子”的模型是后面所有字符串、GC、struct 与 class 问题的共同底座。2.2 三种 string 写法差的是“便签”还是“箱子”回到那道经典题用书桌和仓库的模型一看就清楚了。string str;如果写在方法里编译器会直接忽略这个变量书桌上连便签都不放不分配任何内存如果它是类的成员变量则会被默认赋成 null等价于第三种写法。string str ;是书桌上放了便签仓库里也真的放了一个“空字符串”的箱子便签指向它。string str null;则是书桌上放了便签但便签上什么都没写指针值为 0仓库里根本没有对应的箱子。三者的差别不在“看起来都空”而在于便签有没有、箱子有没有。面试官问这题就是在确认你分得清“引用为空”和“引用指向一个空内容对象”。2.3 不可变性为什么 string 改一次就换一个箱子string是不可变类型这是它和StringBuilder分道扬镳的根本原因。你写string a abcd; a eftg;第二句并不是把仓库里“abcd”那个箱子里的字擦掉重写而是重新在仓库里放一个新箱子“eftg”再把书桌上的便签改指向新箱子。原来那个“abcd”箱子如果没人再引用就等着 GC 来收。所以大量做a xxx这类拼接时string 会频繁申请新箱子、丢弃旧箱子速度自然慢。StringBuilder则相反它初始化时就预留了一块较大的空间追加内容时只要预留空间够就在原箱子里往后写便签始终指向同一个箱子属于典型的“用空间换时间”。判断标准很简单字符串内容基本不变用 string需要在一个循环里反复拼接、频繁改动用 StringBuilder。// string每次拼接都可能产生新对象 string result ; for (int i 0; i 1000; i) { result i.ToString(); // 每次都新建字符串对象 } // StringBuilder预留空间内原地追加 var sb new System.Text.StringBuilder(); for (int i 0; i 1000; i) { sb.Append(i); // 空间足够时不重新分配 } string final sb.ToString();上面两段代码功能一样差别在内存行为前者循环里不断产生临时字符串对象后者在内部缓冲区里追加。参数上StringBuilder可以在构造时传入初始容量预估最终长度能进一步减少扩容次数不传则用默认容量超出后自动翻倍扩容。这也是为什么面试题常追问“什么情况下用 StringBuilder”——答案不是“字符串长就用”而是“改动频繁就用”。3. 值类型与引用类型struct、class、装箱拆箱的易混地带3.1 struct 和 class一个住书桌一个住仓库struct是值类型class是引用类型这是 C# 类型系统的第一道分水岭。用前面的模型说struct 的实例通常直接放在栈上作为字段时随宿主走赋值时是把整个内容复制一份class 的实例本体在堆上变量里存的是地址赋值时复制的是地址两个变量可能指向同一个箱子。这带来一连串差异struct 不能继承类但可以实现接口且默认是密封的struct 的构造函数有约束如果自己写了构造函数就必须给每个成员赋值不能有析构函数class 则没有这些限制。选型上轻量、短生命周期、语义上像“一个值”的比如坐标、金额适合 struct需要继承、需要引用语义、体积较大的适合 class。3.2 装箱与拆箱值类型“搬进仓库”的代价装箱是把值类型转成引用类型拆箱是反过来。通俗理解装箱就是本来在书桌上的东西为了当“对象”用专门在仓库里新建一个箱子把它装进去再把便签给你拆箱就是照着便签去仓库把东西取出来。这个过程有内存分配和类型检查的开销所以频繁装箱是性能隐患。一个容易被忽略的例子是Object.ReferenceEquals(i, j)比较两个 int比较前两个 int 各自被装箱成不同对象堆地址不同结果自然是“不相等”。这也解释了为什么值类型比较内容要用或Equals而不是比引用。3.3 const 与 readonly两种“不能改”不是一回事const和readonly都表示不可修改但时机和范围不同。const是编译期常量声明时必须赋值之后任何地方都不能改可以用于类的成员常量和局部常量readonly是运行期只读只能用于类的成员变量声明时可以不赋值除了在构造函数里还能再赋一次值其他地方都不能改。打个比方const 像刻在石头上的字编译时就定死了readonly 像贴了封条的箱子允许你在“出厂装配”构造函数时放东西封条贴上后就不能再动。还有一个常考细节const不能用static修饰因为常量本身就和类型关联不存在实例级常量。4. 面向对象与运行时接口、抽象类、GC 与委托的边界4.1 接口与抽象类契约和半成品的区别接口和抽象类都能定义“子类该长什么样”但定位不同。抽象类可以有抽象成员没有实现也可以有非抽象成员带实现相当于一个“半成品基类”接口只声明成员不含实现相当于一份“行为契约”。继承上一个类只能继承一个抽象类但可以实现多个接口。选择依据很实在多个子类之间有大量重复代码想复用就用抽象类把公共实现放进非抽象方法只是想让不同类都具备某组能力、且需要多实现就用接口。一句话抽象类解决“是什么、共享代码”接口解决“能做什么、统一契约”。4.2 GC 与内存垃圾没人指向的箱子会被清走GC 是垃圾收集器负责自动回收堆上不再被引用的对象。用模型说当栈上所有便签都不再指向仓库里某个箱子时那个箱子就成了“垃圾”GC 启动扫描堆区时会把它销毁。比如一个方法里object a new object();方法执行完局部变量 a 随栈弹出堆上那个对象若没有别的引用就等着被回收。这也是托管代码不用手动释放内存的原因。但要注意GC 管的是“托管堆上的对象”不是所有资源文件句柄、数据库连接这类非托管资源仍然要靠using或显式释放这也是using关键字除了引入命名空间之外的第二个含义——让实现了IDisposable的对象在作用域结束时自动调用Dispose。4.3 委托与事件函数也能被“存起来”委托可以理解为“封装了函数指针的集合”它能指向一组具有相同签名的方法调用委托就等于调用它指向的方法。事件是一种特殊的委托本质还是委托只是加了访问限制通常只允许外部订阅和退订不允许外部直接触发。这个区别在面试里常被追问“事件是不是一种委托”——答案是“是但是受约束的委托”。理解委托的价值在于它让方法可以像数据一样被传递是回调、事件驱动、LINQ 表达式的基础。5. 常见误区这五个概念最容易张冠李戴5.1 误区一把“引用为空”当成“空字符串”易混点null和都“看起来空”。为什么混输出或判断时两者都可能让界面显示空白。怎么区分null表示便签上没有地址访问它的成员会抛空引用异常是一个真实存在的、长度为 0 的字符串对象可以安全调用Length等方法。判断字符串是否“有内容”要同时考虑这两种情况。5.2 误区二以为 string 赋值是“改内容”易混点a 新值看起来像修改了原字符串。为什么混变量名没变直觉上以为对象也没变。怎么区分string 不可变赋值只是让变量指向新对象旧对象原封不动。所以“传 string 进方法后方法内重新赋值外部变量不变”正是因为方法内改的是副本便签的指向。5.3 误区三把 finally 当成“return 之后才执行”易混点try里有returnfinally到底跑不跑、什么时候跑。为什么混直觉上 return 就“结束”了。怎么区分finally一定会执行而且是在 return 表达式求值之后、方法真正返回之前执行。所以try { return i; } finally { i 100; }最终返回的仍是原来的值finally 里改 i 不影响已确定的返回值。5.4 误区四认为静态方法里能用实例成员易混点静态方法和实例方法写在一起看起来都能访问字段。为什么混语法上都在同一个类里。怎么区分静态方法属于类不属于某个实例没有this只能访问静态成员访问实例字段会编译报错。这是那道“静态函数里给实例字段赋值”错题的核心。5.5 误区五把重载当成小事易混点只重载不重载!。为什么混觉得相等判断写一个就够了。怎么区分C# 要求成对重载重载就必须重载!否则编译不通过同时通常还要重写Equals和GetHashCode保证相等语义一致。这是运算符重载里最常见的踩坑点。6. 什么时候该用这些知识一个判断清单和我的习惯这些概念不是用来背的而是用来做判断的。写代码时遇到字符串拼接先问“改得频不频繁”频繁就上 StringBuilder设计类型时先问“它是值语义还是引用语义、要不要继承”答案决定 struct 还是 class设计层级时先问“是共享代码还是统一契约”答案决定抽象类还是接口处理资源时先问“是不是非托管资源”是就别只指望 GC老老实实using。下面这张表可以把最容易混的几组快速对照对比项前者后者判断要点string / StringBuilder不可变每次改动新建对象可变预留空间内原地追加改动频率struct / class值类型复制内容引用类型复制地址语义与继承需求抽象类 / 接口可有实现单继承只有契约多实现复用还是契约const / readonly编译期常量必须声明时赋值运行期只读构造函数可赋值赋值时机ref / out传参前必须赋值传参前可不赋值是否要求先初始化也有明确“别用”的时候不要为了省事把大对象做成 struct复制成本会反噬不要在循环里对 string 做大量不要用as之后不判空就直接调成员不要以为有了 GC 就永远不用管资源释放。我自己刚工作时就吃过亏写了个循环拼接日志字符串的代码数据量一上来直接卡住后来改成 StringBuilder 才顺过来。从那以后我每次看到循环里的字符串拼接都会强制自己停下来想一遍内存里到底发生了什么。这份资料把这类问题按概念串了起来适合你对照着把认知补完整希望帮到你。本文还有配套的精品资源点击获取