我曾经认为
Record
是(不可变)数据的容器,直到我遇到了一些启发性的读物。
鉴于函数可以被视为 F# 中的值,记录字段也可以保存函数值。这为状态封装提供了可能性。
module RecordFun =
type CounterRecord = {GetState : unit -> int ; Increment : unit -> unit}
// Constructor
let makeRecord() =
let count = ref 0
{GetState = (fun () -> !count) ; Increment = (fun () -> incr count)}
module ClassFun =
// Equivalent
type CounterClass() =
let count = ref 0
member x.GetState() = !count
member x.Increment() = incr count
用法
counter.GetState()
counter.Increment()
counter.GetState()
看起来,除了继承之外,使用
Class
能做的事情并不多,而使用 Record
和辅助函数则做不到。其中更适合函数式概念,例如模式匹配、类型推断、高阶函数、泛型相等...
进一步分析,
Record
可以看作是由makeRecord()
构造函数实现的接口。应用(某种)关注点分离,可以更改
makeRecord
函数中的逻辑,而不会有破坏 contract(即记录字段)的风险。
当用与类型名称匹配的模块替换
makeRecord
函数时,这种分离变得明显(参考 Christmas Tree Record)。
module RecordFun =
type CounterRecord = {GetState : unit -> int ; Increment : unit -> unit}
// Module showing allowed operations
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module CounterRecord =
let private count = ref 0
let create () =
{GetState = (fun () -> !count) ; Increment = (fun () -> incr count)}
问:记录应该被视为简单的数据容器还是状态封装有意义?我们应该在哪里划清界限,什么时候应该使用
Class
而不是 Record
?
请注意,链接帖子中的模型是纯粹的,而上面的代码则不是。
我认为这个问题没有一个通用的答案。确实,记录和类在某些潜在用途上是重叠的,您可以选择其中任何一个。
值得记住的一个区别是,编译器会自动生成记录的结构相等和结构比较,这是类无法免费获得的东西。这就是为什么记录是“数据类型”的明显选择。
在记录和类别之间进行选择时我倾向于遵循的规则是:
{ ... with ... }
语法创建克隆,请使用记录。如果您正在编写一些递归处理并且需要保持状态,这特别好。我认为并不是每个人都会同意这一点,并且它并没有涵盖所有选择 - 但一般来说,使用数据记录和其余的本地类型和类似乎是在两者之间进行选择的合理方法。
如果你想实现记录中的数据隐藏,我觉得有更好的方法,比如抽象数据类型“模式”。
看看这个:
type CounterRecord =
private {
mutable count : int
}
member this.Count = this.count
member this.Increment() = this.count <- this.count + 1
static member Make() = { count = 0 }
Make
成员,count
字段是可变的 - 不是什么值得骄傲的事情,但我想说你的反例是公平的。此外,由于 private 修饰符,无法从定义它的模块外部访问它。要从外部访问它,您拥有只读 Count
属性。 Increment
函数可以改变内部状态。CounterRecord
实例 - 正如托马斯提到的,记录的卖点。 至于记录作为接口,你可能会看到有时在现场,尽管我认为这更像是JavaScript/Haskell 的习惯用法。与这些语言不同,F# 具有 .NET 的接口系统,与对象表达式结合使用时会变得更加强大。我觉得没有太多理由为此重新调整记录的用途。