如何精简F#中长度受限字符串的重复代码?
实现强类型的长度受限字符串(消除重复代码)
在数据处理场景里,我们经常需要严格控制字符串长度——不管是适配固定宽度的文本输出,还是存入有限长度的nvarchar数据库字段。这时候用强类型代替裸的System.String会更安全,但之前的实现方式太冗余了,比如下面的重复模块:
module String40 = let private MaxLength = 40 type T = private T of string let create (s:string) = checkStringLength MaxLength s |> Result.map T let trustCreate (s:string) = checkStringLength MaxLength s |> Result.okVal |> T let truncateCreate (s:string) = truncateStringToLength MaxLength s |> T let toString (T s) = s type T with member this.AsString = this |> toString module String100 = let private MaxLength = 100 type T = private T of string let create (s:string) = checkStringLength MaxLength s |> Result.map T let trustCreate (s:string) = checkStringLength MaxLength s |> Result.okVal |> T let truncateCreate (s:string) = truncateStringToLength MaxLength s |> T let toString (T s) = s type T with member this.AsString = this |> toString
你看,String100和String40几乎一模一样,就模块名和MaxLength值不同,维护起来简直是噩梦。我一直想把它精简成type String40 = LengthLimitedString<40>这种清爽的形式,期间试过几种思路都踩了坑:
- T4代码生成:F#项目不支持,直接行不通;
- Type Provider:复杂度太高,而且没法生成我们需要的模块结构;
- Scott Wlaschin的约束字符串方案:还是避免不了重复代码的问题。
最后我用常规泛型约束实现了一个可行的方案,代码示例如下:
type IMaxLengthProvider = abstract member GetMaxLength: unit -> int type MaxLength3 () = interface IMaxLengthProvider with member this.GetMaxLength () = 3 type MaxLength4 () = interface IMaxLengthProvider with member this.GetMaxLength () = 4 module LimitedString = type T<'a when 'a :> IMaxLengthProvider> = private T of string let create<'t when 't :> IMaxLengthProvider and 't : (new:unit -> 't)> (s:string) = let len = (new 't()).GetMaxLength() match checkStringLength len s with | Ok s -> let x : T<'t> = s |> T x |> Ok | Error e -> Error e let trustCreate<'t when 't :> IMaxLengthProvider and 't : (new:unit -> 't)> (s:string) = let len = (new 't()).GetMaxLength() match checkStringLength len s with | Ok s -> let x : T<'t> = s |> T x | Error e -> let msg = e |> formErrorMessage failwith msg let truncateCreate<'t when 't :> IMaxLengthProvider and 't : (new:unit -> 't)> (s:string) = let len = (new 't()).GetMaxLength() let s = truncateStringToLength len s let x : T<'t> = s |> T x let toString (T s) = s type T<'a> with member this.AsString = this |> toString
(注:原代码里toStri是截断内容,这里补全了toString及对应的成员定义)
这个方案虽然需要实例化IMaxLengthProvider的实现类来获取长度,但对使用者来说非常简洁,扩展也容易。最重要的是,它能让我们的记录类型保持强类型区分,比如可以定义:
// 先封装类型别名 type String40 = LimitedString.T<MaxLength40> type String100 = LimitedString.T<MaxLength100> // 再定义业务记录 type MyRecord = { FirstName: String40; LastName: String100; }
完全不用担心类型混淆,完美满足强类型的安全需求。
内容的提问来源于stack exchange,提问作者Clyde
相关产品推荐
相关产品推荐

