第4章 高频C#基础面试题:值类型与引用类型、装箱拆箱、GC原理、异常处理

本章聚焦C#高频基础面试三大板块:值/引用类型内存差异、装箱拆箱性能陷阱以及GC回收机制与异常处理。掌握这些概念,能让你在面试中对“内存管理”“性能优化”类问题游刃有余。

核心知识速查表 — 面试前快速过一遍
主题 核心概念 常见面试问题 优化策略
值类型 vs 引用类型 栈/堆分配;复制值 vs 复制引用 struct/class 选型;默认值;性能差异 大结构体用 class;频繁传递用 ref/in
装箱 / 拆箱 值类型 → object 转换;隐式分配堆对象 触发场景;性能损失;如何避免 泛型替代;避免 ArrayList;结构体实现接口
GC 原理(代际) 0/1/2 代;Mark-Compact;触发条件 提高GC效率;大对象堆;固定对象 减少 short-lived 对象;使用对象池
异常处理 try-catch-finally;性能开销;自定义异常 何时用何种 catch;finally 执行;异常筛选 避免吞异常;使用 when 子句;异步异常处理
1. 内存:栈 vs 堆 2. 赋值:值复制/引用复制 3. 默认值:0/null 4. 继承:sealed/可继承 5. 性能:值快但注意大结构
💡 面试加分 回答值/引用类型时,主动提及 Span<T> 和 Memory<T> 对栈分配的优化,能体现对 .NET 8 新特性的关注。

问答卡片 01:值类型与引用类型的本质区别

面试问题

请详细解释值类型和引用类型的区别,并举例说明它们的内存分配方式。

面试官考察点
  • 是否理解栈/堆分配原理
  • 能否说出值类型赋值是复制内容、引用类型赋值是复制引用
  • 是否了解 struct 与 class 的实际场景选择
回答框架
  1. 先定义:值类型(struct/int/bool)在栈上分配;引用类型(class/string/array)在堆上分配,栈只存引用地址。
  2. 对比赋值行为:值类型复制整个数据,引用类型复制引用地址。
  3. 补充默认值与继承特性,并给出性能建议。
示例回答

值类型存储在栈上,赋值时复制全部字段,性能好但大结构体应谨慎。引用类型存储在堆上,赋值只复制引用,适合大对象和需要多引用的场景。默认值上,值类型为 0 或 null?实际值类型没有 null(Nullable 包装除外),引用类型为 null。

可能追问
  • Q:string 是值类型吗?为什么它表现出值类型的行为?
  • Q:struct 可以实现接口吗?为什么?
  • Q:Span<T> 与数组在内存上的区别?
⚠️ 避坑提醒 不要回答“值类型分配在栈上,引用类型分配在堆上”就结束。更完整的回答需要说明引用类型的对象本身在堆上,但引用地址在栈上。另外,值类型作为局部变量时确实在栈上,但如果作为引用类型的字段,则被存储在堆上(随父对象一起)。

问答卡片 02:装箱与拆箱的原理与优化

面试问题

什么是装箱和拆箱?在什么场景下会发生?如何避免其性能损耗?

面试官考察点
  • 是否知道装箱实质是值类型 → 引用类型的隐式转换,会分配堆对象
  • 能否识别常见装箱点(ArrayList、非泛型集合、值类型调用继承自 object 的方法)
  • 是否有实际优化经验(泛型、结构体实现接口避免装箱)
回答框架
  1. 定义:值类型转为 object 或接口类型时发生装箱,反之拆箱需要显式转换。
  2. 举例典型场景:ArrayList.Add(5)、value.ToString() 并无装箱,但 ((IComparable)5).CompareTo(...) 会装箱。
  3. 优化方向:使用泛型集合、让值类型实现接口避免装箱、用 Nullable<T> 替代 object。
示例回答

装箱将值类型复制到堆上的托管对象中,增加 GC 压力。可以通过泛型(List<int>)替代 ArrayList,或者让结构体显式实现接口来避免隐式箱子转换。拆箱时注意类型匹配,否则抛异常。

可能追问
  • Q:int?.GetValueOrDefault() 是否会有装箱?
  • Q:值类型实现接口时,接口调用是否必然装箱?如何避免?
  • Q:在 .NET 8 中有什么新机制减少装箱?
⚠️ 避坑提醒 不要错误地说 value.ToString() 会装箱——实际上 ToString() 是值类型的方法,不触发装箱。另外,拆箱时一定要检查类型是否匹配,否则 InvalidCastException 是常见坑。

问答卡片 03:GC 代际回收与内存管理

面试问题

请解释 .NET 垃圾回收器的代际原理(0代/1代/2代),并说明 GC 触发的条件和调优手段。

面试官考察点
  • 是否理解分代假设:大部分对象生命周期短
  • 能否描述 Mark-Compact 以及大对象堆(LOH)的处理
  • 是否了解 GC.TryStartNoGCRegion、GC.Collect 等调优方法的适用场景
回答框架
  1. 代际设计:0 代新对象,回收频率高;1 代中转;2 代和 LOH 长存,回收代价大。
  2. 触发条件:内存不足、对象分配触发 0 代满、显式调用 GC.Collect、LOH 碎片化等。
  3. 调优方向:减少短生命周期对象(对象池)、避免大对象频繁分配、使用 ArrayPool、合理设置 GC 模式(Server/Workstation)。
示例回答

GC 基于分代回收:0 代回收最快,大部分临时对象在此释放。当 0 代满且分配请求无法满足时触发回收,幸存对象升到 1 代。大对象(≥85KB)在 LOH,不压缩但可能碎片,需避免频繁分配。可采用 ArrayPool 复用大数组来减少 LOH 压力。

可能追问
  • Q:GC 回收时线程挂起(Stop-the-World)发生在哪些代?如何降低影响?
  • Q:如何判断一个对象是否仍在 GC 引用根上?
  • Q:C# 中异常处理对 GC 有哪些影响?例如 finally 是否会延迟对象回收?
⚠️ 避坑提醒 不要过度依赖 GC.Collect 手动回收,会打乱分代策略,降低整体性能。另外,异常 finally 中如果持有对象引用,会影响 GC 判断,需及时释放资源(using 或 try-dispose)。