十年的 .NET 开发经验教给你的不仅是语法——更是生存技能。
我维护过看似靠胶带粘合的代码库,曾在凌晨三点调试竞态条件,也曾因终于理解了一个空引用异常而欢呼。
这不是又一份“最佳实践”清单,而是经过实战检验的真理——用惨痛教训换来的经验,让你不必重蹈覆辙。
让我们开始吧。
我职业生涯早期曾构建过一个 API,它在发送响应前会将所有客户记录加载到内存中。一开始运行良好——直到用户量突破 10 万,服务器崩溃了。
后来我发现了 IAsyncEnumerable<T>
。它无需将所有数据倒入 List<T>
,而是按需流式传输数据。内存再也不会爆炸,凌晨也不会再收到运维的紧急告警了。
适用场景:
yield return
过于同步的场景我曾花了四个小时调试一个问题,仅仅因为有人硬编码了:
var setting = config["Api:Endpoint"];
后来键名改了。没有编译时错误,只有运行时的静默失败。
现在我只用:
IOptions<T>
)nameof()
实现反射安全引用未来的你会感谢自己。
并非每个异步方法都需要 Task
。如果你的方法经常同步完成(例如缓存命中),ValueTask
可以避免不必要的堆分配。
经验法则:
Task
ValueTask
我的第一次生产事故?Socket 耗尽。
我曾经天真地创建 HttpClient
实例:
using (var client = new HttpClient()) { ... }
大错特错。HttpClient
不会立即释放底层 Socket。
解决方案?
services.AddHttpClient(); // 使用 IHttpClientFactory
现在框架负责管理连接池和生命周期。问题解决。
曾经用 string.Split()
解析过大型 CSV 文件吗?GC 会恨你。
Span<T>
让你无需分配内存即可切割字符串和数组:
ReadOnlySpan<char> slice = bigString.AsSpan(start, length);
适用场景:
我曾经很喜欢 TryParse 模式,直到看到这个:
if (int.TryParse(input, out var value)) { ... }
现在,用 C# 7 的模式匹配:
if (input is int value) { ... }
更简洁、更安全,再也没有 out
参数潜入你的逻辑。
写过这样的辅助方法吗?
void Assert(bool condition, string message) { ... }
现在用 C# 10:
void Assert(bool condition, [CallerArgumentExpression("condition")] string expr = null)
{
Console.WriteLine($"Failed: {expr}");
}
调用 Assert(x > 0)
时会输出 "Failed: x > 0"。再也不用猜测哪个检查失败了。
我曾用 dynamic
来“简化”JSON 解析器。
两个月后,我们遇到了:
替代方案:
System.Text.Json
当我们的 Web 集群缓存状态不同步时,我惨痛地学到了这一点。
规则:
IMemoryCache
= 单服务器,内存缓存IDistributedCache
(Redis、SQL Server)= 多服务器缓存别搞混了。
职业生涯早期,我通过到处撒 Console.WriteLine
来“调试”。
后来我发现了:
DebuggerDisplayAttribute
实现更清晰的调试器视图你的终端不应该是调试器。
需要重型对象但只是偶尔使用?
private readonly Lazy<ExpensiveService> _service = new Lazy<ExpensiveService>(() => new ExpensiveService());
只有在首次访问时才会初始化。完美适用于:
C# 10 引入了 record struct
。与 record
(引用类型)不同,这是值类型。
何时使用?
我曾经写:
void Process(string input)
{
if (input == null) throw new ArgumentNullException(nameof(input));
...
}
现在用 C# 11:
void Process(string input!!)
{
...
}
!!
自动添加空值检查。更少的样板代码。
死锁。哦,那些死锁。
我曾经在同步上下文中调用 Task.Result
导致整个 ASP.NET 应用冻结。
始终优先使用:
await someTask;
如果必须阻塞,使用:
someTask.GetAwaiter().GetResult();
(但说实话,尽量不要阻塞。)
厌倦了辅助方法堆满堆栈跟踪?
[StackTraceHidden]
void ThrowHelper() => throw new InvalidOperationException();
现在堆栈跟踪会直接跳转到真正的问题。
曾经尝试过对使用 DateTime.Now
的代码进行单元测试吗?简直是噩梦。
.NET 8 引入了 TimeProvider
——一个强大的抽象来解决这个问题。
你不能直接实例化 TimeProvider,因为它是抽象类。而是使用静态实例或注入可模拟版本。
示例:
var now = TimeProvider.System.GetUtcNow();
示例(使用模拟的测试代码): 你可以使用伪造/模拟实现,比如 FakeTimeProvider,在测试中模拟时间
var fakeTime = new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero);
var fakeProvider = new FakeTimeProvider();
fakeProvider.SetUtcNow(fakeTime);
var now = fakeProvider.GetUtcNow(); // 返回你控制的测试时间
这消除了所有时间相关的不可靠性,使测试变得确定性。
职业生涯早期,我跳过了“无聊”的主题,比如:
大错特错。
我认识的最优秀的 .NET 开发者都深入理解运行时。他们不只是写代码——而是与运行时合作。
这些不是建议——而是伤疤。
每一条都源自深夜的紧急故障处理、生产环境事故或“这玩意儿为什么这么慢?”的时刻。
最好的 .NET 开发者不只是程序员;他们是能从每个缺陷中学习的问题解决者。
去创造令人惊叹的东西吧——或许还能顺便避免一些我犯过的错误。