1. 引言:那些被误解的C#特性
作为一名有十年C#开发经验的工程师,我经常在代码审查中发现一些"约定俗成"的错误用法。这些特性看似简单,却隐藏着许多开发者在日常工作中容易忽视的细节。今天我们就来盘点13个最容易被误用的C#特性,其中不少问题我在团队协作和开源项目贡献中都曾反复遇到。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. async/await的常见陷阱
2.1 同步上下文捕获问题
最常见的错误是在UI线程中使用.Result或.Wait()阻塞调用:
csharp复制// 错误示例
var result = GetDataAsync().Result;
这会导致死锁,因为async/await默认会捕获同步上下文。正确的做法应该是:
csharp复制// 正确用法
var result = await GetDataAsync();
提示:在库代码中应考虑使用
ConfigureAwait(false)避免不必要的上下文捕获
2.2 异常处理差异
许多开发者不知道async方法中的异常会被包装在AggregateException中:
csharp复制try {
await Task.WhenAll(task1, task2);
} catch (AggregateException ex) {
// 实际会进入这里
}
3. LINQ的误用场景
3.1 过早物化查询
典型的性能杀手是过早调用ToList():
csharp复制// 低效写法
var filtered = db.Products.ToList().Where(p => p.Price > 100);
应该保持查询延迟执行:
csharp复制// 优化写法
var query = db.Products.Where(p => p.Price > 100);
3.2 FirstOrDefault的null检查
很多人这样写:
csharp复制var product = products.FirstOrDefault();
if (product != null) {...}
但对于值类型集合,这会引发装箱:
csharp复制// 更优方案
if (products.Any()) {
var product = products.First();
}
4. Record类型的深层机制
4.1 相等性比较的误区
Record的==运算符实际调用的是Equals方法:
csharp复制var p1 = new Person("John");
var p2 = new Person("John");
Console.WriteLine(p1 == p2); // true
但如果是继承关系:
csharp复制Person p = new Student("John");
Console.WriteLine(p == new Student("John")); // false
4.2 可变性陷阱
虽然record设计为不可变,但可以这样"修改":
csharp复制var person = person with { Name = "New Name" };
5. Nullable Reference Types的实战细节
5.1 注解与运行时行为
很多人以为string?会在运行时阻止null赋值:
csharp复制string? name = null; // 编译警告但运行通过
实际这只是编译时检查,需要配合#nullable enable。
5.2 泛型场景的特殊处理
对于泛型参数,T?的含义取决于T:
csharp复制T? Method<T>(T? input) where T : class => input; // 引用类型
T? Method<T>(T? input) where T : struct => input; // 可空值类型
6. 多线程编程的隐蔽问题
6.1 lock对象的选用
常见错误是锁定this或字符串:
csharp复制lock("myLock") { ... } // 危险!
应该使用专用对象:
csharp复制private readonly object _lockObj = new object();
lock(_lockObj) { ... }
6.2 线程安全集合的误区
即使使用ConcurrentDictionary也不意味着所有操作都原子:
csharp复制if (!dict.ContainsKey(key))
dict.TryAdd(key, value); // 非原子操作
应该使用:
csharp复制dict.GetOrAdd(key, k => value);
7. 反射的性能优化
7.1 缓存Type实例
每次调用GetType()都有开销:
csharp复制// 低效
MethodInfo method = obj.GetType().GetMethod("MyMethod");
// 优化
private static readonly Type myType = typeof(MyClass);
MethodInfo method = myType.GetMethod("MyMethod");
7.2 表达式树替代
对于高频调用的反射,可改用表达式树编译:
csharp复制var param = Expression.Parameter(typeof(T));
var prop = Expression.Property(param, "MyProperty");
var lambda = Expression.Lambda<Func<T, object>>(prop, param);
var getter = lambda.Compile();
8. 委托与事件的微妙区别
8.1 事件封装原则
很多人混淆委托字段和事件:
csharp复制public Action OnEvent; // 不是事件!
public event Action OnRealEvent; // 真正的事件
区别在于事件提供了封装性。
8.2 空事件检查模式
传统空检查:
csharp复制if (OnEvent != null) OnEvent();
现代C#可简化为:
csharp复制OnEvent?.Invoke();
9. 泛型约束的高级用法
9.1 new()约束的局限
只能用于有无参构造器的类型:
csharp复制where T : new()
但无法指定参数化构造。
9.2 非托管类型约束
C# 7.3引入:
csharp复制where T : unmanaged
可用于优化低层代码。
10. 模式匹配的进阶技巧
10.1 属性模式
可以匹配对象属性:
csharp复制if (obj is { Length: > 5 }) {...}
10.2 递归模式
C# 10增强:
csharp复制if (e is { Message: { Length: > 10 } }) {...}
11. 插值字符串的隐藏成本
11.1 装箱问题
插值字符串会导致值类型装箱:
csharp复制int num = 42;
string s = $"The answer is {num}"; // 装箱发生
11.2 FormattableString保留格式
可以使用:
csharp复制FormattableString fs = $"Today is {DateTime.Now}";
12. using声明的新语义
12.1 隐式作用域
C# 8引入的using声明:
csharp复制using var file = new StreamReader("file.txt");
会在作用域结束时自动释放。
12.2 异步释放
C# 8也支持:
csharp复制await using var resource = new AsyncDisposable();
13. 顶级语句的注意事项
13.1 隐式Main方法
看似简单的脚本:
csharp复制Console.WriteLine("Hello");
实际会被编译为完整的Program类。
13.2 命名空间冲突
顶级语句文件中不能声明命名空间。
14. 实战经验总结
在多年的C#开发中,我发现这些特性误用最常见于以下场景:
- 从旧版本迁移的代码库
- 缺乏完整代码审查的紧急项目
- 混合不同C#版本特性的代码
特别建议:
- 定期用Roslyn分析器检查代码质量
- 新项目应启用所有最新语言特性
- 重要项目考虑采用Nullable Reference Types
关于async/await的一个实用技巧:在ASP.NET Core中,控制器方法应该始终返回Task而不是void,否则异常处理会出问题。我曾在一个电商项目中因此丢失了关键的错误日志。
对于LINQ查询,我习惯在调试时保留非物化查询,直到真正需要结果时才调用ToList()。这在大数据量处理时能显著提升性能,我们在一个数据分析项目中通过这种方式减少了40%的内存使用。
