F#记录与班级

Fun*_*unk 10 oop f# types

我曾经认为a 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
Run Code Online (Sandbox Code Playgroud)

用法

counter.GetState()
counter.Increment()
counter.GetState()
Run Code Online (Sandbox Code Playgroud)

似乎除了继承之外,Class用a Record和helper函数做的事情并没有多少.其功能概念更好,例如模式匹配,类型推断,高阶函数,通用等式......

进一步分析,Record可以看作是构造函数实现的接口makeRecord().应用(某种)关注点分离,其中makeRecord函数中的逻辑可以被改变而没有违反合同的风险,即记录字段.

makeRecord使用与类型名称匹配的模块(ref圣诞树记录)替换函数时,这种分离变得明显.

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)}
Run Code Online (Sandbox Code Playgroud)

问:是否应将记录视为数据的简单容器,或者状态封装是否有意义?我们应该在哪里绘制线,何时应该使用Class而不是Record

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

Tom*_*cek 11

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

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

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

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

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


scr*_*wtp 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 }
Run Code Online (Sandbox Code Playgroud)
  • 记录构造函数是私有的,因此构造实例的唯一方法是通过静态Make成员,
  • count这个领域是可变的 - 不是值得骄傲的事情,但我会说你公平的比赛例子.此外,由于私有修饰符,它无法从模块外部访问.要从外部访问它,您具有只读Count属性.
  • 就像你的例子一样,Increment记录中有一个函数可以改变内部状态.
  • 与您的示例不同,您可以CounterRecord使用自动生成的结构比较来比较实例 - 正如Tomas所提到的,记录的卖点.

至于记录作为接口,你可能会看到有时 在现场,虽然我认为它更像是一个JavaScript/Haskell成语.与那些语言不同,F#具有.NET的接口系统,与对象表达式结合使用时更加强大.我觉得没有太多理由为此重新调整记录.