用 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<t></t>和
IAsyncEnumerable<t></t>混用
用 Option<t></t>
类型模拟 F# 的 option
,但必须配合命名约束
C# 没有原生
option,但可用开源库(如
LanguageExt)或手写
Option<t></t>。问题在于:开发者常把它当“更安全的 nullable”,却忽略其核心价值——显式声明“这个值可能不存在”,并强制调用方处理两种情况。
容易踩的坑:
把Option<t></t>当作
T?的替代品,在方法签名里隐藏了“空可能性”,导致调用方仍用
.Value强解包 未禁用隐式转换(如
default(T)→
None),让
null偷渡进逻辑链 在 EF Core 查询中滥用
Option<t></t>,触发客户端求值(
ClientEval)甚至运行时异常
关键不是加一个类型,而是让每个
Some/
None分支都有对应语义动作。比如
user.Email.ToOption().Map(EmailService.Send)比
if (user.Email != null) EmailService.Send(user.Email)更难被跳过。
