F# 记录与班级

问题描述 投票:0回答:2

我曾经认为

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

请注意,链接帖子中的模型是纯粹的,而上面的代码则不是。

oop types f#
2个回答
16
投票

我认为这个问题没有一个通用的答案。确实,记录和类在某些潜在用途上是重叠的,您可以选择其中任何一个。

值得记住的一个区别是,编译器会自动生成记录的结构相等和结构比较,这是类无法免费获得的东西。这就是为什么记录是“数据类型”的明显选择。

在记录和类别之间进行选择时我倾向于遵循的规则是:

  • 使用数据类型记录(免费获得结构平等)
  • 当我想提供 C# 友好或 .NET 风格的公共 API(例如带有可选参数)时,请使用类。你也可以用记录来做到这一点,但我发现类更简单
  • 对本地使用的类型使用记录 - 我认为您通常最终会直接使用记录(例如创建它们),因此添加/删除字段需要更多工作。对于仅在单个文件中使用的记录来说,这不是问题。
  • 如果我需要使用
    { ... with ... }
    语法创建克隆,请使用记录。如果您正在编写一些递归处理并且需要保持状态,这特别好。

我认为并不是每个人都会同意这一点,并且它并没有涵盖所有选择 - 但一般来说,使用数据记录和其余的本地类型和类似乎是在两者之间进行选择的合理方法。


6
投票

如果你想实现记录中的数据隐藏,我觉得有更好的方法,比如抽象数据类型“模式”。

看看这个:

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 的接口系统,与对象表达式结合使用时会变得更加强大。我觉得没有太多理由为此重新调整记录的用途。

© www.soinside.com 2019 - 2024. All rights reserved.