这13个C# 特性你可能一直在用错
摘要:文章总结了13个C#开发中常见的高频误区,包括async/await误用、var滥用、LINQ副作用、资源未释放等问题。这些问题往往因开发者对特性理解不足或使用随意导致,虽不会立即引发故障,但会随着系统规模扩大演变为性能隐患。作者通过真实案例(如文件句柄泄漏导致下载变慢)强调正确使用语言特性的重要性,建议开发者深入理解每个特性的设计初衷,避免因滥用特性带来潜在风险。
前段时间排查过一次线上问题:网站下载文件突然变得异常缓慢,网络、磁盘都没问题,最后才发现是文件句柄长期占用却没有释放。问题并不复杂,只是少了一个 using,但影响却实实在在。修复之后,下载速度立刻恢复正常,这也让我再次意识到——很多 C# 的“坑”,并不是不会写,而是太熟悉以至于写得不够谨慎。
C# 是一门不断进化、极其成熟的语言。从 async/await、LINQ,到 record、nullable reference types,它几乎覆盖了现代开发的所有范式。但也正因为功能强大,很多特性在日常开发中被“用得很顺手,却用得并不正确”。
这些问题往往不会立刻引发 Bug,却会在系统规模扩大后,逐渐演变为性能隐患、维护负担,甚至架构级问题。下面这 13 个点,几乎都是小编在真实项目和代码评审中反复见过的“高频误区”。
1. async/await 并不等于多线程
这是最常见、也最容易被误解的一点。async/await 的核心目标是避免线程阻塞,而不是提升 CPU 并行能力。如果你在异步方法里执行的是 CPU 密集型任务,它依然会占用当前线程。
// 错误:CPU 计算仍然阻塞当前线程
await DoCpuBoundWorkAsync();
// 正确:显式把 CPU 工作交给线程池
await Task.Run(() => DoCpuBoundWork());
正确的心智模型是:I/O 密集型 → 原生 async/awaitCPU 密集型 → Task.Run 或并行计算
2. var 既不是洪水猛兽,也不是银弹
有些团队完全禁止 var,有些则无脑全用。其实这两种都是极端。
// 可读性差:类型来源不清晰
var data = GetData();
// 更清楚:一眼就知道是什么
List<User> users = GetUsers();
经验法则很简单:当右侧表达式已经“自解释”时,用 var;否则明确写类型。
3. IEnumerable的延迟执行很容易被忽视
IEnumerable<T> 的惰性执行是 LINQ 的核心特性之一,但在实际项目中,它经常成为性能陷阱。
// 危险:如果 GetUsers() 是数据库查询,这里会执行两次
var users = GetUsers();
var count = users.Count();
var first = users.First();
一旦你确定要多次使用结果,就应该主动物化:
var users = GetUsers().ToList(); // 只执行一次
4. LINQ 不应该承担副作用逻辑
LINQ 是为查询和转换设计的,而不是为了“顺手干点别的”。
// 反模式:在 Select 中写副作用逻辑
items.Select(x =>
{
SaveToDatabase(x);
return x;
}).ToList();
真正清晰、可维护的写法反而更“朴素”:
foreach (var item in items)
{
SaveToDatabase(item);
}
代码不是写给编译器看的,是写给同事和未来的自己看的。
5. 混用 ==、Equals 和 ReferenceEquals
在 C# 中,== 是可以被重载的,它并不总是表示“引用相等”。
// 语义明确的写法
if (obj1.Equals(obj2))
{
// 逻辑相等
}
如果你关心的是“是不是同一个对象”,那就应该使用 ReferenceEquals。 自定义类型中,一定要**成对重写 Equals 和 ==**,否则迟早踩坑。
6. 把异常当成正常流程的一部分
异常机制非常昂贵,用它来处理“预期情况”几乎一定是反模式。
// 低效且不必要
try
{
int.Parse(input);
}
catch
{
// ignore
}
更合理的方式是:
if (int.TryParse(input, out var value))
{
// 使用 value
}
异常,应该只用于真正的“异常”。
7. 忘记释放 IDisposable 资源
凡是实现了 IDisposable 的类型,都不应该靠 GC “顺手回收”。
// 风险代码:文件句柄可能长期占用
var stream = new FileStream(path, FileMode.Open);
现代 C# 的推荐写法非常干净:
using var stream = new FileStream(path, FileMode.Open);
8. readonly 并不等于不可变
这是一个语义陷阱。readonly 只能保证引用不变,不能保证对象状态不变。
// 看起来安全,其实内容仍可修改
readonly List<int> numbers = new();
如果你真正关心的是不可变性,应该考虑:
IReadOnlyList<int> numbers = new List<int>();
或直接使用 ImmutableList<T>。
9. 忽略 Nullable Reference Types
如果你还在新项目中关闭可空引用类型,那基本等于主动放弃了一层“编译期防御”。
#nullable enable
string? name = null;
string displayName = name ?? "Unknown";
这是极少数几乎没有副作用,却能显著提升代码质量的特性。
10. 把 record 当成普通 class 用
record 的设计目标是不可变、值语义的数据结构。
// 反模式:破坏 record 的设计初衷
public record User
{
public string Name { get; set; }
}
正确姿势是:
public record User(string Name);
如果你需要复杂行为和可变状态,那它本来就该是 class。
11. 过度依赖 async void
async void 只应该用于事件处理器。
// 风险:异常无法被捕获
async void DoWork()
{
await Task.Delay(1000);
}
除非你非常确定,否则**一律使用 async Task**。
12. 在 ASP.NET Core 中滥用 Task.Run
Web 应用中随意 Task.Run,往往是在和线程池“硬碰硬”。
// 多数情况下并不推荐
await Task.Run(() => DoSomething());
ASP.NET Core 本身就是高并发模型,除非是 CPU 密集型计算,否则应直接使用异步 API。
13. 滥用 static 共享状态
static 很方便,但也是并发问题的温床。
static List<string> Cache = new();
在多线程环境中,这几乎必然导致数据竞争。 需要共享状态时,请优先考虑 依赖注入 + 生命周期管理。
结语
C# 从来不缺“高级特性”,真正稀缺的是对边界和代价的清醒认知。 很多代码的问题,并不是不会写,而是写得太随意、太自信。
语言特性本身没有对错,错的是在不了解设计初衷的情况下滥用它。 当你开始思考“我为什么要用这个特性”,而不是“我能不能用”,你的代码质量就已经领先大多数人了。大家有没有在项目中用错的C# 特性导致网站变慢?欢迎留言讨论。
更多推荐
所有评论(0)