十年 .NET 老兵的血泪教训:这 17 个实战经验让你少走弯路

作者:微信公众号:【架构师老卢】
8-26 19:14
10

十年的 .NET 开发经验教给你的不仅是语法——更是生存技能。

我维护过看似靠胶带粘合的代码库,曾在凌晨三点调试竞态条件,也曾因终于理解了一个空引用异常而欢呼。

这不是又一份“最佳实践”清单,而是经过实战检验的真理——用惨痛教训换来的经验,让你不必重蹈覆辙。

让我们开始吧。


1. IAsyncEnumerable 是你的流式处理秘密武器

我职业生涯早期曾构建过一个 API,它在发送响应前会将所有客户记录加载到内存中。一开始运行良好——直到用户量突破 10 万,服务器崩溃了。

后来我发现了 IAsyncEnumerable<T>。它无需将所有数据倒入 List<T>,而是按需流式传输数据。内存再也不会爆炸,凌晨也不会再收到运维的紧急告警了。

适用场景:

  • 大型数据集查询
  • 实时事件流处理
  • 任何觉得 yield return 过于同步的场景

2. 魔法字符串是沉默的杀手

我曾花了四个小时调试一个问题,仅仅因为有人硬编码了:

var setting = config["Api:Endpoint"];

后来键名改了。没有编译时错误,只有运行时的静默失败。

现在我只用:

  • 强类型配置(IOptions<T>
  • 用常量或枚举表示事件名、权限等
  • nameof() 实现反射安全引用

未来的你会感谢自己。


3. 关键场景用 ValueTask 替代 Task

并非每个异步方法都需要 Task。如果你的方法经常同步完成(例如缓存命中),ValueTask 可以避免不必要的堆分配。

经验法则:

  • 真正的异步操作(数据库调用、HTTP 请求)用 Task
  • 可能异步但经常同步完成的方法用 ValueTask

4. 永远不要直接实例化 HttpClient

我的第一次生产事故?Socket 耗尽。

我曾经天真地创建 HttpClient 实例:

using (var client = new HttpClient()) { ... }

大错特错。HttpClient 不会立即释放底层 Socket。

解决方案?

services.AddHttpClient(); // 使用 IHttpClientFactory

现在框架负责管理连接池和生命周期。问题解决。


5. 用 Span 实现零开销字符串操作

曾经用 string.Split() 解析过大型 CSV 文件吗?GC 会恨你。

Span<T> 让你无需分配内存即可切割字符串和数组:

ReadOnlySpan<char> slice = bigString.AsSpan(start, length);

适用场景:

  • 高性能解析
  • 协议缓冲区/字节操作
  • 避免子字符串分配

6. out 关键字是代码异味

我曾经很喜欢 TryParse 模式,直到看到这个:

if (int.TryParse(input, out var value)) { ... }

现在,用 C# 7 的模式匹配:

if (input is int value) { ... }

更简洁、更安全,再也没有 out 参数潜入你的逻辑。


7. 用 [CallerArgumentExpression] 实现智能调试

写过这样的辅助方法吗?

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"。再也不用猜测哪个检查失败了。


8. 像躲避瘟疫一样避免 dynamic

我曾用 dynamic 来“简化”JSON 解析器。

两个月后,我们遇到了:

  • 拼写错误导致的运行时崩溃
  • 零 IDE 支持
  • 20% 的性能损失

替代方案:

  • 使用强类型模型的 System.Text.Json
  • JSON 序列化的源生成器

9. MemoryCache 不是分布式缓存

当我们的 Web 集群缓存状态不同步时,我惨痛地学到了这一点。

规则:

  • IMemoryCache = 单服务器,内存缓存
  • IDistributedCache(Redis、SQL Server)= 多服务器缓存

别搞混了。


10. Console.WriteLine 是测试反模式

职业生涯早期,我通过到处撒 Console.WriteLine 来“调试”。

后来我发现了:

  • DebuggerDisplayAttribute 实现更清晰的调试器视图
  • 结构化日志(Serilog/NLog)
  • 单元测试断言

你的终端不应该是调试器。


11. 用 Lazy 处理昂贵初始化

需要重型对象但只是偶尔使用?

private readonly Lazy<ExpensiveService> _service = new Lazy<ExpensiveService>(() => new ExpensiveService());

只有在首次访问时才会初始化。完美适用于:

  • 数据库连接
  • 插件加载器
  • 大型配置

12. 用 record struct 进行微优化

C# 10 引入了 record struct。与 record(引用类型)不同,这是值类型。

何时使用?

  • 小型不可变数据包
  • 需要关注分配的高性能场景

13. 空值检查应该显式声明

我曾经写:

void Process(string input)  
{  
    if (input == null) throw new ArgumentNullException(nameof(input));  
    ...  
}

现在用 C# 11:

void Process(string input!!)  
{  
    ...  
}

!! 自动添加空值检查。更少的样板代码。


14. 避免 Task.Result 和 Task.Wait()

死锁。哦,那些死锁。

我曾经在同步上下文中调用 Task.Result 导致整个 ASP.NET 应用冻结。

始终优先使用:

await someTask;

如果必须阻塞,使用:

someTask.GetAwaiter().GetResult();

(但说实话,尽量不要阻塞。)


15. 用 [StackTraceHidden] 保持错误日志整洁

厌倦了辅助方法堆满堆栈跟踪?

[StackTraceHidden]  
void ThrowHelper() => throw new InvalidOperationException();

现在堆栈跟踪会直接跳转到真正的问题。


16. 用 TimeProvider 实现可测试的时间逻辑

曾经尝试过对使用 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(); // 返回你控制的测试时间

这消除了所有时间相关的不可靠性,使测试变得确定性。


17. 再资深的开发者也不能忽视基础

职业生涯早期,我跳过了“无聊”的主题,比如:

  • 垃圾回收内部机制
  • JIT 优化
  • IL 编织

大错特错。

我认识的最优秀的 .NET 开发者都深入理解运行时。他们不只是写代码——而是与运行时合作。


最后的话

这些不是建议——而是伤疤。

每一条都源自深夜的紧急故障处理、生产环境事故或“这玩意儿为什么这么慢?”的时刻。

最好的 .NET 开发者不只是程序员;他们是能从每个缺陷中学习的问题解决者。

去创造令人惊叹的东西吧——或许还能顺便避免一些我犯过的错误。

相关留言评论
昵称:
邮箱:
阅读排行