应优先用 record 替代 class 声明不可变数据模型,因其自动生成值语义比较、ToString 和 with 表达式;避免可变字段,慎用 record struct;用 switch 表达式替代 if-else 链;纯计算用同步方法或 ValueTask.FromResult;Option 需显式处理 Some/None 分支。

用 record 替代传统 class 声明不可变数据模型
F# 默认鼓励不可变性,而 C# 从 9.0 开始引入 record 类型,正是对这一思想的直接响应。它自动生成值语义比较(Equals / GetHashCode)、ToString 和非破坏性修改(with 表达式),省去大量样板代码。
实操建议:
- 优先用
record表示 DTO、领域事件、配置快照等只读数据载体,而非仅当“看起来简单”时才用 - 避免在
record中声明可变字段或属性 setter —— 这会削弱其语义一致性,也容易让团队误以为它是“轻量 class” -
record struct在高频小对象场景(如点坐标、颜色)下能减少 GC 压力,但要注意它不支持继承和虚方法
用 switch 表达式替代 if-else if 链处理多态数据
F# 的模式匹配天然支持类型、结构、常量组合判断;C# 8+ 的 switch 表达式虽未达 F# 级别,但已足够覆盖多数“根据类型/值分支”的场景,且强制穷尽(配合 not null 和 when 可逼近)。
常见错误现象:仍用嵌套 if 判断 obj is TypeA a && a.Status == X,导致逻辑分散、遗漏分支、难以测试。
实操建议:
- 对
object或基类变量做运行时类型分发时,优先写switch (obj)+case TypeA a when a.Status == X: - 搭配
sealed层级的继承体系(如abstract record+ 具体record子类),能让编译器提示未覆盖的 case - 避免在
switch表达式中混用语句块({})和表达式 —— 统一返回值类型更利于推导和重构
用 ValueTask + async 风格封装纯计算逻辑
F# 的异步工作流(async { ... })本质是描述计算步骤,不绑定线程;C# 的 async 方法默认调度到 ThreadPool,但若函数只是 CPU-bound 转换(如 JSON 解析后映射为 record),用 Task.Run 反而增加开销。
实操建议:
- 对纯函数式转换(输入确定 → 输出确定,无副作用),直接写同步方法;需要“假装异步”以适配接口时,用
ValueTask.FromResult(result),而非Task.FromResult或Task.Run - 警惕
async void和未 await 的async调用 —— 它们破坏了函数式“可组合性”,也让错误传播不可控 - 在 LINQ 链中混合异步操作(如
SelectAsync)时,明确区分“数据流”与“控制流”,避免把IEnumerable和IAsyncEnumerable混用
用 Option 类型模拟 F# 的 option,但必须配合命名约束
C# 没有原生 option,但可用开源库(如 LanguageExt)或手写 Option。问题在于:开发者常把它当“更安全的 nullable”,却忽略其核心价值——显式声明“这个值可能不存在”,并强制调用方处理两种情况。
容易踩的坑:
- 把
Option当作T?的替代品,在方法签名里隐藏了“空可能性”,导致调用方仍用.Value强解包 - 未禁用隐式转换(如
default(T)→None),让null偷渡进逻辑链 - 在 EF Core 查询中滥用
Option,触发客户端求值(ClientEval)甚至运行时异常
关键不是加一个类型,而是让每个 Some/None 分支都有对应语义动作。比如 user.Email.ToOption().Map(EmailService.Send) 比 if (user.Email != null) EmailService.Send(user.Email) 更难被跳过。











