F#记录与班级

时间:2016-12-31 17:05:08

标签: oop types f#

我曾经认为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和帮助函数做。哪个plays better with functional concepts,如模式匹配,类型推断,高阶函数,泛型相等......

进一步分析,Record可以被视为makeRecord()构造函数实现的接口。应用(排序)关注点分离,makeRecord函数中的逻辑可以更改,而不会有违反契约的风险,即记录字段。

当使用与类型名称匹配的模块替换makeRecord函数时,这种分离变得明显(参考圣诞节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

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

2 个答案:

答案 0 :(得分:10)

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

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

在记录和选择之间我倾向于遵循的规则课程是:

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

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

答案 1 :(得分:6)

如果你想在记录中实现隐藏数据,我觉得有更好的方法可以实现,例如abstract data type&#34; pattern&#34;。

看看这个:

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字段是可变的 - 不是值得骄傲的事情,但我会说你的反面例子公平的游戏。此外,由于私有修饰符,它无法从模块外部访问。要从外部访问它,您将拥有只读Count属性。
  • 与您的示例一样,记录中的Increment函数会改变内部状态。
  • 与您的示例不同,您可以使用自动生成的结构比较来比较CounterRecord实例 - 正如Tomas所提到的,记录的卖点。

至于作为接口的记录,您可能会看到sometimes in the field,但我认为它更像是一个JavaScript / Haskell成语。与那些语言不同,F#具有.NET的接口系统,与object expressions结合使用时更加强大。我觉得没有太多理由为此重新调整记录。