前段时间排查过一次线上问题:网站下载文件突然变得异常缓慢,网络、磁盘都没问题,最后才发现是文件句柄长期占用却没有释放。问题并不复杂,只是少了一个 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# 特性导致网站变慢?欢迎留言讨论。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐