【问题标题】:When should I use a record over a tuple?我什么时候应该在元组上使用记录?
【发布时间】:2021-10-30 01:12:01
【问题描述】:

我正在尝试在 F# 中实践领域驱动设计,偶然发现了以下问题:

当使用元组似乎需要更少的语法并且在模式匹配和整体使用场景方面似乎更强大时,为什么我要使用记录?

例如,如果我要使用元组代替,我觉得我不再需要创建记录类型来实现区分联合。

type Name = { First:string 
              Middle:string option
              Last:string }

type Duration = { Hours:int
                  Minutes:int
                  Seconds:int }

type Module = 
    | Author of Name
    | Title of string
    | Duration of Duration

let tryCreateName (first, middle, last) =
    { First=first; Middle=Some middle; Last=last }

let tryCreateDuration (hours, minutes, seconds) =
    { Hours=hours; Minutes=minutes;Seconds=seconds }

let name = tryCreateName ("Scott", "K", "Nimrod")

let hours = 1
let minutes = 30
let seconds = 15

let duration = tryCreateDuration (hours, minutes, seconds)

我的想法准确吗?

在大多数情况下,是否需要元组而不是记录?

【问题讨论】:

  • 我会回答,但我知道托马斯会做得更好。 IIRC 它与使用 C# 和 .NET 有更多关系。如果您纯粹是在 F# 中,那么请使用元组。如果您正在使用其他 .NET 代码,请使用记录。还有其他因素,例如它是否带有记录,如果它不是公开的,是否带有元组等。另外,在您询问之前,请参阅链接末尾的When to Use Classes, Unions, Records, and Structures
  • 你看到F# tag中的所有信息了吗?
  • @ Guy Coder - 感谢您的链接。该指南似乎支持您关于为何使用记录的指导。谢谢。

标签: f#


【解决方案1】:

对于领域建模,我建议使用带有命名元素的类型;也就是记录、可区分的联合,也许还有偶尔的类或接口。

在结构上,记录和元组是相似的;用代数数据类型的说法,它们都是产品类型

不同之处在于,对于元组,值的顺序很重要,每个元素的作用是隐含的。

> (2016, 1, 2) = (2016, 1, 2);;
val it : bool = true
> (2016, 1, 2) = (2016, 2, 1);;
val it : bool = false

在上面的示例中,您可能会猜到这些元组模拟日期,但究竟是哪些?是 2016 年 1 月的第二天吗?还是 2016 年 2 月的第一天?

另一方面,对于记录,元素的顺序无关紧要,因为您可以通过名称绑定它们并访问它们:

> type Date = { Year : int; Month : int; Day : int };;

type Date =
  {Year: int;
   Month: int;
   Day: int;}

> { Year = 2016; Month = 1; Day = 2 } = { Year = 2016; Day = 2; Month = 1 };;
val it : bool = true

当你想提取组成值时也更清楚。您可以轻松地从记录值中获取年份:

> let d = { Year = 2016; Month = 1; Day = 2 };;

val d : Date = {Year = 2016;
                Month = 1;
                Day = 2;}

> d.Year;;
val it : int = 2016

从元组中提取值要困难得多:

> let d = (2016, 1, 2);;

val d : int * int * int = (2016, 1, 2)

> let (y, _, _) = d;;

val y : int = 2016

当然,对于对,您可以使用内置函数 fstsnd 来访问元素,但是对于具有三个或更多元素的元组,除非您进行模式匹配,否则您无法轻松获取值.

即使您定义了自定义函数,每个元素的角色仍然由其序号隐式定义。如果它们属于同一类型,则很容易将值的顺序弄错。

所以对于领域建模,我总是更喜欢显式类型,以便清楚发生了什么。

那么,元组永远不合适吗?

元组在其他情况下也很有用。当您需要使用 ad hoc 类型 来组合函数时,它们比记录更合适。

Seq.zip 为例,它可以让您组合两个序列:

let alphalues = Seq.zip ['A'..'Z'] (Seq.initInfinite ((+) 1)) |> Map.ofSeq;;

val alphalues : Map<char,int> =
  map
    [('A', 1); ('B', 2); ('C', 3); ('D', 4); ('E', 5); ('F', 6); ('G', 7);
     ('H', 8); ('I', 9); ...]

> alphalues |> Map.find 'B';;
val it : int = 2

正如您在此示例中所见,元组只是朝着实际目标迈出的一步,即按字母顺序排列的值的映射。如果我们想在表达式中组合值时必须定义记录类型,那将是很尴尬的。元组非常适合这项任务。

【讨论】:

  • “另一方面,对于记录,元素的顺序无关紧要”...有时确实很重要,例如,当记录的实例是比较或排序,因为记录默认提供结构比较。
  • @MarcSigrist 你能详细说明一下吗?如上所示,{ Year = 2016; Month = 1; Day = 2 } 等于 { Year = 2016; Day = 2; Month = 1 },即使列出元素的顺序不同。
  • 元素的顺序在记录类型声明中很重要,因为编译器生成的 IComparable 实现依赖于它。
  • @MarcSigrist 感谢您的解释。我不知道。
  • 我同意记录通常更方便,因为从它们中提取值更容易,但它们对于类型推断不太好:如果你想“点入”记录,你通常需要显式声明其类型。
【解决方案2】:

您可能对optional parameters 感兴趣,它只允许在类型上使用。为避免更改参数的顺序,我也将姓氏设为可选(也许它还可以更好地模拟您的问题域,例如在遇到 Mononyms 时)。

为了表示两个时间点之间的差异,框架中已经有一个库函数System.TimeSpan

因此你的例子可以写成

type Name = {
    FirstName   : string 
    MiddleName  : string option
    LastName    : string option } with
    static member Create(firstName, ?middleName, ?lastName) =
        {   FirstName   = firstName
            MiddleName  = middleName
            LastName    = lastName }

type System.TimeSpan with
    static member CreateDuration(hours, ?minutes, ?seconds) = 
        System.TimeSpan(
            hours,
            defaultArg minutes 0,
            defaultArg seconds 0 )

let name = Name.Create("Scott", "K", "Nimrod")
let duration = System.TimeSpan.CreateDuration(1, 30, 15)

这个想法是提供extension methods,它接受不同长度的元组参数,并返回一个复杂的数据结构,例如记录或结构。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-07
    • 2011-08-08
    • 2012-09-22
    • 1970-01-01
    • 2010-12-30
    相关资源
    最近更新 更多