我的应用程序中有两种实体:客户和产品。它们每个都由UUID在数据库级别标识。
在我的F#代码中,可以用System.Guid
表示。
为了便于阅读,我添加了一些类似的类型:
open System
type CustomerId = Guid
type ProductId = Guid
但是,这并不妨碍我将ProductId
用作CustomerId
,反之亦然。
我想出了一个包装器主意来防止这种情况:
open System
[<Struct>]
type ProductId =
{
Product : Guid
}
[<Struct>]
type CustomerId =
{
Customer : Guid
}
这使初始化更加冗长,并且可能不那么直观:
let productId = { Product = Guid.NewGuid () }
但是它增加了类型安全性:
// let customerId : CustomerId = productId // Type error
我想知道还有什么其他方法。
答案 0 :(得分:5)
您可以使用single-case union类型:
open System
[<Struct>]
type ProductId = ProductId of Guid
[<Struct>]
type CustomerId = CustomerId of Guid
let productId = ProductId (Guid.NewGuid())
通常我们直接在类型中添加一些方便的辅助方法/属性:
[<Struct>]
type ProductId = private ProductId of Guid with
static member Create () = ProductId (Guid.NewGuid())
member this.Value = let (ProductId i) = this in i
[<Struct>]
type CustomerId = private CustomerId of Guid with
static member Create () = CustomerId (Guid.NewGuid())
member this.Value = let (CustomerId i) = this in i
let productId = ProductId.Create ()
productId.Value |> printfn "%A"
答案 1 :(得分:4)
另一种较不常见的方法,但值得一提的是使用所谓的幻像类型。这个想法是,您将拥有一个通用包装ID<'T>
,然后对'T
使用不同的类型来表示不同类型的ID。这些类型从未真正实例化,这就是为什么它们被称为 phantom 类型。
[<Struct>]
type ID<'T> = ID of System.Guid
type CustomerID = interface end
type ProductID = interface end
现在,您可以创建ID<CustomerID>
和ID<ProductID>
值来表示两种ID:
let newCustomerID () : ID<CustomerID> = ID(System.Guid.NewGuid())
let newProductID () : ID<ProductID> = ID(System.Guid.NewGuid())
有趣的是,您可以编写可轻松使用任何ID的函数:
let printID (ID g) = printfn "%s" (g.ToString())
例如,我现在可以创建一个客户ID,一个产品ID并同时打印两者,但是由于它们的类型不匹配,我无法对这些ID进行相等性测试:
let ci = newCustomerID ()
let pi = newProductID ()
printID ci
printID pi
ci = pi // Type mismatch. Expecting a 'ID<CustomerID>' but given a 'ID<ProductID>'
这是一个巧妙的技巧,但是比为每个ID使用新类型要复杂得多。特别是,您可能会在各个地方需要更多的类型注释来使这项工作有效,并且类型错误可能不太清楚,尤其是当涉及通用代码时。但是,值得一提的是它。