F#中如何为复合度量单位类型成员设置合法取值范围限制
实现方案
核心思路是通过私有化类型构造器+智能构造函数的组合,从根源上避免非法值被创建,完全符合「让非法状态无法表达」的设计原则:
- 将复合单位类型的构造器私有化,禁止外部直接创建实例
- 实现智能构造函数,输入任意英石/磅、英尺/英寸数值时自动做归一化校验和转换,确保输出的结果永远符合取值规则
- 可额外提供从总磅、总英寸直接生成复合单位的便捷方法,适配不同使用场景
完整代码实现
首先补全基础单位模块的转换常量:
module Units // Mass [<Measure>] type kg // Kilogram [<Measure>] type g // Gram [<Measure>] type lb // Pound (mass) [<Measure>] type st // Stone (mass) // 补充单位转换常量 let lbPerSt = 14.0<lb/st> let kgPerLb = 0.45359237<kg/lb> // Length [<Measure>] type m // Metre [<Measure>] type cm // Centimetre [<Measure>] type inch // Inch [<Measure>] type ft // Foot // 补充单位转换常量 let inchPerFt = 12.0<inch/ft> let cmPerInch = 2.54<cm/inch>
复合单位模块的安全实现:
module CompoundUnits open Units // 私有化构造器,外部无法直接创建实例 type StonesAndPounds = private { Stones: float<st> Pounds: float<lb> } with // 智能构造函数,自动归一化 static member create (stones: float<st>) (pounds: float<lb>) = // 先转换为总磅数做统一计算 let totalLb = stones * lbPerSt + pounds // 处理负值,可根据业务需求调整为返回Result类型代替抛出异常 if totalLb < 0.0<lb> then invalidArg (nameof totalLb) "重量不能为负值" // 归一化计算 let normalizedSt = System.Math.Floor(float (totalLb / lbPerSt)) * 1.0<st> let normalizedLb = totalLb % lbPerSt { Stones = normalizedSt; Pounds = normalizedLb } // 便捷方法:从总磅数直接创建 static member fromTotalPounds (totalLb: float<lb>) = StonesAndPounds.create 0.0<st> totalLb // 便捷方法:转换为总磅数方便后续换算 member this.TotalPounds = this.Stones * lbPerSt + this.Pounds // 英尺英寸的实现逻辑完全一致 type FeetAndInches = private { Feet: float<ft> Inches: float<inch> } with static member create (feet: float<ft>) (inches: float<inch>) = let totalInch = feet * inchPerFt + inches if totalInch < 0.0<inch> then invalidArg (nameof totalInch) "长度不能为负值" let normalizedFt = System.Math.Floor(float (totalInch / inchPerFt)) * 1.0<ft> let normalizedInch = totalInch % inchPerFt { Feet = normalizedFt; Inches = normalizedInch } static member fromTotalInches (totalInch: float<inch>) = FeetAndInches.create 0.0<ft> totalInch member this.TotalInches = this.Feet * inchPerFt + this.Inches
使用示例
你提到的非法输入会被自动归一化为符合日常表达习惯的格式:
// 自动归一为8英石11磅 let eightStoneEleven = StonesAndPounds.create 6.0<st> 39.0<lb> // 自动归一为5英尺10英寸 let fiveFootTen = FeetAndInches.create 4.0<ft> 22.0<inch>
如果希望更贴合纯函数式的错误处理风格,也可以把构造函数的返回值改成Result<复合单位类型, string>类型代替抛出异常,上层调用方可以自行处理非法输入的分支。
内容的提问来源于stack exchange,提问作者JansthcirlU
相关产品推荐
相关产品推荐

