用途
fsharp-library-api-design 面向已发布 F# 库的 API 维护者,目标是在修改公共 API 时不破坏使用方。技能把已发布库的契约分为两种:
- 源码兼容性:针对 1.0.0 编译过的 F# 和 C# 代码,能针对 1.1.0 继续编译。
- 二进制兼容性:针对 1.0.0 编译的程序集,能在 1.1.0 下运行。
NuGet 场景经常出现后一种情况:包 A 基于 1.0.0 构建,应用需要 1.1.0,A 不重新编译,直接随 1.1.0 运行。技能强调,次要版本和补丁版本必须同时保持这两份契约;只做一次干净的完整重编译,只能证明源码兼容性。
主要能力
技能以表格形式列出对 1.0.0 做出各类变更后,在 F# 源码、C# 源码、二进制和包验证四个方面的结果,涉及:新增联合类型分支、新增记录字段、柯里化函数改为元组、添加 RequireQualifiedAccess、inline 函数体或小型函数体变更、参数泛化或类型放宽、新增可选参数、在公共接口上新增成员、模块重命名、添加 CompiledName、添加 Obsolete、新增重载等。表中指出,部分 F# 破坏性变更表面上不像破坏性变更,且包验证不会报告(例如新增联合类型分支、柯里化改元组),而某些变更会导致 MissingMethodException、TypeLoadException 或 CP0002/CP0001/CP0006 等验证错误。
使用方式
技能给出以下工作步骤:
- 变更前记录已构建库的公共接口快照;若安装了 fsharp-signature-files,可用其 public-surface.ps1。
- 列出请求涉及的每项公共变更,对照表格判断是否破坏契约;若破坏,采用安全替代方案,或将版本升级为主版本。
- 开启针对上一发布版本的包验证:
<PropertyGroup>
<EnablePackageValidation>true</EnablePackageValidation>
<PackageValidationBaselineVersion>1.0.0</PackageValidationBaselineVersion>
</PropertyGroup>
执行 dotnet pack 后,若 1.0.0 的类型或成员签名被删除或更改,会报 CP0001、CP0002 或 CP0006;但它不会报告纯 F# 源码层面的破坏、新增联合分支或函数体变更。
4. 用现有使用方检查两种契约:源码方面,针对新库构建每个 F# 与 C# 使用方及其测试;二进制方面,先用旧版本构建使用方,再把新库 DLL 及新依赖 DLL 复制到其输出目录并运行,输出必须不变。
5. 对照快照比较公共接口,审查新增的 C# 可调用接口是否符合面向 C# 的规范。
6. 确定版本号并撰写发布说明,列出每项新增、每个 Obsolete 及 FSharp.Core 版本变化。
验证时可从技能根目录运行 pwsh ./scripts/run.ps1。该脚本会把 scripts/V1 打包为 1.0.0 基线,对 scripts/changes 中每个变更版本开启包验证打包,用其构建 F# 和 C# 使用方,并用新 DLL 运行 1.0.0 使用方,再将各项结果与变更中 // expect- 行比较,不一致时以退出码 1 结束;它还会检查固定与不固定版本时的 FSharp.Core 依赖。
规范要点
技能规定若干设计规则:以扩展代替修改;不要在次要版本中修改公共函数或成员签名(包括新增可选参数、放宽或泛化参数类型);新增重载不得使现有调用产生歧义,否则对 C# 的 client.Send(null) 会触发 CS0121,对 F# 未标注函数参数会触发 FS0041;模块函数不能重载,需新增不同名称的函数;记录新增字段会使旧构造函数消失;联合新增分支会使对旧二进制的匹配可能走错分支且无报错,重编译使用方则可能抛 MatchFailureException;不要向公共接口添加成员,可新建继承旧接口的接口,并在运行时用 :? 检测。
对 C# 可调用的新公共成员:参数避免使用 option、voption、list、Map、Set、函数类型和 Async,改用 seq<'T>、数组、IReadOnlyList<'T>、Func 委托,或用 null/重载表示缺省;不要向 C# 返回 option、voption、函数类型或 Async,用 null 或带 outref 的 TryX 方法,返回委托而非 F# 函数,边界返回 Task<'T>;需 C# 可省略的参数使用 [<Optional; DefaultParameterValue(...)>],且仅用于新成员;C# 调用的新模块函数加 [<CompiledName("PascalCase")>],但不要给已有函数添加。
版本策略按 SemVer:主版本允许上表任何破坏(包括新增 Obsolete);次要版本允许新类型、新函数、不引起歧义的新重载、类的新成员;补丁版本只做不改变公共接口的修复,并说明 inline 或小型函数体修复需使用方重编译才生效。技能还建议固定最低 FSharp.Core 版本:
<ItemGroup>
<PackageReference Update="FSharp.Core" Version="6.0.7" />
</ItemGroup>
若需让小型函数体修复无需使用方重编译即生效,可在首次发布时加入 --nooptimizationdata:
<OtherFlags>$(OtherFlags) --nooptimizationdata</OtherFlags>
适用场景与限制
适用于维护已发布 F# 库、需要评估 API 变更兼容性并按语义化版本发版的场景。技能自身说明,部分结论不在本技能证明范围内:新增联合分支的错误分支与 MatchFailureException 来自 fsharp-binary-compatibility 的 union-new-case 证明;F# 不能为接口成员声明默认实现;references/CHANGES.md 中的 IStore2 示例未被运行器编译;运行器仅向两个版本同时传入 // flags: 行,因此不能证明仅在 1.1.0 中设置的标志会让旧使用方保留跨程序集内联复制的主体。