C# 语言核心与底层机制
在 .NET 面试中,面试官往往不会直接询问语法,而是通过考察底层机制来评估候选人对 CLR (Common Language Runtime) 的理解深度。以下整理了面试中最高频的几个核心考点。
1. 值类型与引用类型的内存布局
理解栈(Stack)与堆(Heap)的区别是基础中的基础。在 .NET 中,值类型(如 int, struct, enum)通常分配在栈上,而引用类型(如 class, interface, array)的实例分配在堆上。然而,需要注意的是,局部变量即使是指针,其本身也存储在栈上。
核心差异对比
| 特性 | 值类型 (Value Type) | 引用类型 (Reference Type) |
|---|---|---|
| 存储位置 | 通常在线程栈上 | 托管堆 (Heap) |
| 赋值行为 | 复制整个值 | 复制引用地址 |
| 默认值 | 0 或 null | null |
| 继承 | 隐式继承 System.ValueType | 隐式继承 System.Object |
性能陷阱:装箱 (Boxing)
装箱是将值类型转换为 object 类型或由此值类型实现的接口类型的过程。这是一个内存分配操作,会在堆上分配一个新对象并将值复制过去。拆箱则是相反的过程,开销较小但仍有成本。
// 示例:装箱操作 int i = 123; object o = i; // 装箱:堆上创建新对象 i = 456; // 修改 i 不会影响 o // 避免装箱的最佳实践:使用泛型 List<int> numbers = new List<int>(); // 不会装箱 List<object> objects = new List<object>(); // 会装箱
2. 异步编程模型 (async/await)
现代 .NET 开发中,异步编程是必须掌握的技能。面试常问:`async/await` 到底释放了什么资源?
答案核心在于:它释放了线程池线程 (Thread Pool Thread),而不是释放了内存。当遇到 `await` 时,当前的执行上下文会被捕获,线程返回线程池,直到异步操作完成,再从线程池获取一个线程继续执行后续代码。
public async Task<string> GetDataAsync()
{
// 线程 A 释放,回到线程池
var result = await httpClient.GetStringAsync("https://api.example.com");
// 线程 B 继续执行(可能是线程 A,也可能是其他线程)
return result;
}
LeetCode 高频算法题 (C# 实现)
对于 .NET 岗位,算法题不仅考察逻辑,还考察对 C# 集合类 (Collections) 和 Linq 的运用能力。以下是三个经典题目的深度解析。
题目:两数之和 (Two Sum)
问题描述: 给定一个整数数组 `nums` 和一个目标值 `target`,请你在该数组中找出和为目标值的那两个整数,并返回他们的数组下标。
解题思路: 暴力解法时间复杂度为 O(n²),面试中通常要求优化至 O(n)。利用 Dictionary 存储已遍历的数字及其索引,可以在一次遍历中完成查找。
public int[] TwoSum(int[] nums, int target) {
var map = new Dictionary<int, int>();
for (int i = 0; i < nums.Length; i++) {
int complement = target - nums[i];
if (map.ContainsKey(complement)) {
return new int[] { map[complement], i };
}
map[nums[i]] = i;
}
throw new ArgumentException("No two sum solution");
}
题目:反转链表 (Reverse Linked List)
问题描述: 输入一个头节点,反转链表并返回新的头节点。
解题思路: 使用三个指针 `prev`, `current`, `next` 进行迭代反转。这是考察指针操作能力的经典题目。
public ListNode ReverseList(ListNode head) {
ListNode prev = null;
ListNode current = head;
while (current != null) {
ListNode nextTemp = current.next; // 暂存下一个节点
current.next = prev; // 反转指针
prev = current; // prev 前进一步
current = nextTemp; // current 前进一步
}
return prev;
}
题目:二叉树的层序遍历 (Level Order Traversal)
问题描述: 返回二叉树的层序遍历结果(逐层从左到右)。
解题思路: 使用 队列 (Queue) 实现 BFS (广度优先搜索)。利用 C# 的 `Queue<T>` 类可以非常优雅地实现。
public IList<IList<int>> LevelOrder(TreeNode root) {
var result = new List<IList<int>>();
if (root == null) return result;
var queue = new Queue<TreeNode>();
queue.Enqueue(root);
while (queue.Count > 0) {
int levelSize = queue.Count;
var currentLevel = new List<int>();
for (int i = 0; i < levelSize; i++) {
var node = queue.Dequeue();
currentLevel.Add(node.val);
if (node.left != null) queue.Enqueue(node.left);
if (node.right != null) queue.Enqueue(node.right);
}
result.Add(currentLevel);
}
return result;
}
.NET 系统设计与架构思维
高级职位的面试不仅限于代码,更关注架构设计。以下列举了面试中常涉及的设计模式与架构概念。
依赖注入 (DI)
理解 生命周期 (Lifetime) 的区别:Transient(瞬时)、Scoped(作用域)、Singleton(单例)。在 .NET Core 中,DI 容器是内置的,需掌握如何正确注册服务以避免循环依赖和内存泄漏。
中间件管道 (Middleware)
ASP.NET Core 的请求处理管道由一系列中间件组成。面试常问:自定义中间件如何实现?需实现 `Invoke` 方法,并理解 `next()` 调用的时机对性能监控和异常处理的影响。
缓存策略
区分 分布式缓存 (Redis) 与 内存缓存 (IMemoryCache)。掌握缓存穿透、缓存击穿、缓存雪崩的解决方案。例如,使用互斥锁解决缓存击穿。