【问题标题】:F# Record vs ClassF# 记录与类
【发布时间】:2016-12-31 17:05:07
【问题描述】:

我曾经认为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 函数时,这种分离变得明显(参考 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#


【解决方案1】:

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

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

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

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

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

【讨论】:

  • 我同意,这是一个广泛的话题,需要一些有经验的观点。感谢您的见解。
  • 这个答案漏掉了一点,就是性能。如果您查看编译后的代码,类会出现得更快,因为它们删除了记录解决方案将生成的 FSharpFunc 的间接性。
【解决方案2】:

如果你想实现数据隐藏在记录中,我觉得有更好的方法,比如abstract data type"pattern"。

看看这个:

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 结合使用时更加强大。我觉得没有太多理由为此重新调整记录的用途。

【讨论】:

  • 这种封装方式肯定更直接(也更熟悉)。有趣的帖子,谢谢。
  • The downside of the member only approach 是它首先违背了处理记录的目的(模式匹配,类型推断,......)。当然,在CounterRecord 的上下文中,您是绝对正确的。
  • 您链接到的文章大大过分强调了这些缺点,而实际上这些缺点只是轻微的不便。有一个很好的comment by Mauricio Scheffer 指出了这些缺陷。
猜你喜欢
  • 2011-08-17
  • 1970-01-01
  • 2011-05-01
  • 1970-01-01
  • 2013-06-21
  • 1970-01-01
  • 2021-06-06
  • 1970-01-01
  • 2015-05-15
相关资源
最近更新 更多