c#题例-2025-06-30 18:52:58
日期: 2025-06-30 分类: AI写作 10次阅读
当然可以!以下是一道**专家级别**的 C# 逻辑面试题,它不仅考察候选人的语言掌握程度,还涉及对 **委托、事件、异步编程、线程安全、闭包和捕获变量的理解**,非常适合用于高级或资深 C# 程序员的面试。
---
### 🧠 面试题:深入理解 `async/await` 和闭包中的变量捕获
#### 题目描述:
请分析以下代码,并回答输出结果是什么?并解释其背后的原因。
```csharp
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
for (int i = 0; i < 5; i++)
{
Task.Run(async () =>
{
await Task.Delay(100); // 模拟异步操作
Console.WriteLine(i);
});
}
await Task.Delay(1000); // 确保所有任务完成
}
}
```
---
### 💬 问题延伸(进阶讨论):
1. 输出结果可能不是你预期的 `0,1,2,3,4`,为什么?
2. 如何修改这段代码,使其输出为 `0,1,2,3,4`?
3. 如果将 `i` 改成 `string s = i.ToString();` 并打印 `s`,是否会有不同表现?为什么?
4. 如果使用 `ValueTask` 替代 `Task`,会有什么区别?
---
### ✅ 参考答案与解析:
#### 1. 原始输出:
由于 **lambda 表达式中捕获的是变量 i 的引用**,而不是值。所有任务几乎同时启动,并在循环结束后才执行 `Console.WriteLine(i)`,此时 `i` 已经变成 5,所以最终输出可能是多个 `5`。
> ⚠️ 注意:实际输出不一定是全部 5,也可能是部分 5,取决于调度器和延迟时间,但无法保证顺序性和正确性。
#### 2. 修改方法:
为了避免这个问题,你需要在每次循环中创建一个副本,例如:
```csharp
for (int i = 0; i < 5; i++)
{
int local = i;
Task.Run(async () =>
{
await Task.Delay(100);
Console.WriteLine(local);
});
}
```
这样,每个 lambda 捕获的是不同的局部变量副本,从而避免了共享状态的问题。
#### 3. 关于字符串赋值:
如果改成:
```csharp
string s = i.ToString();
Console.WriteLine(s);
```
这不会改变行为,因为 `s` 是基于当前 `i` 创建的,只要 `i` 被正确捕获,就能正常输出。
但如果写成:
```csharp
string s = i.ToString();
await Task.Delay(100);
Console.WriteLine(s);
```
而 `i` 没有被复制,那么仍然会出问题,因为 `i` 的变化会影响 `s` 的值(如果 `i` 在后续被修改)。
#### 4. ValueTask vs Task:
- `ValueTask` 是结构体,在某些场景下性能更好,尤其是当任务很可能已经完成时。
- 但它不能用于长时间运行的任务或需要多次 `await` 的情况。
- 在这个例子中,用 `ValueTask` 不会改变输出结果,但可能影响资源管理和性能。
---
### 🧩 总结考点:
| 考察点 | 说明 |
|--------|------|
| 闭包与变量捕获 | 是否理解 lambda 中变量是按引用还是按值捕获 |
| 异步编程模型 | 是否理解 `async/await` 在多线程环境下的行为 |
| 线程安全问题 | 是否意识到共享变量在并发访问时的风险 |
| 修复技巧 | 是否知道如何通过局部变量来“冻结”当前值 |
| 编译器优化 | 是否了解 C# 编译器在不同版本中对循环变量的处理变化(C#5+ 修复了 foreach,但未修复 for) |
---
如果你希望我再提供一道关于 **LINQ、表达式树、反射、依赖注入、设计模式等结合的专家级题目**,也可以告诉我 😊
除特别声明,本站所有文章均为原创,如需转载请以超级链接形式注明出处:SmartCat's Blog
标签:AI写作
精华推荐