7.1.1 使用 F# 记录类型
记录是带标记元组(labeled tuples)。它他们将多个不同的元素存储在一个值中;此外,每个元素都有一个可以用来访问它的名字。在 F# 中,元素的名字叫字段(fields)。这在很多方面类似于 C 的记录或结构构造,或者 C# 中的匿名类型。与匿名类型不同,记录必须事先声明。类似于匿名类型,记录,在其基本的形式中,包含唯一属性保存数据;清单 7.1 显示了表示矩形的这种声明。
Listing 7.1 Representing a rectangle using a record type (F# Interactive)
> type Rect =
{ Left : float32
Top : float32
Width : float32
Height : float32 };;
type Rect = (...)
> let rc = { Left = 10.0f; Top = 10.0f;
Width = 100.0f; Height = 200.0f; };;
val rc : Rect = (...)
> rc.Left + rc.Width;;
val it : float32 = 110.0f
当声明记录类型时,我们必须指定字段的类型和它的名字。在此例中,我们使用 float32 类型,它对应于 C# 中的 float 和 .NET 的 System.Single 类型,因为,在后面,我们需要这种类型的矩形。要创建 F# 记录的值,在大括号中指定所有字段的值。注意,我们不必写出记录类型名字,这是通过用字段的名字来自动推断的,你可以看到,在我们的示例中,编译器正确地推断出我们要创建矩形类型的值。与 C# 中匿名类型的工作相比,这是不同的。如果编译器基于字段的名字,找不到任何适当的记录类型,将报告错误。
我们在处理记录时,需要读它们的字段,但还需要更改字段的值。例如,向右移动矩形。由于记录是一种函数式的数据结构,它是不可变的,我们将转而不得不用已修改的值创建一个记录。向右移动的矩形记录,写出来可能像这样:
let rc2 = { Left = rc.Left + 100.0f; Top = rc.Top;
Width = rc.Width; Height = rc.Height }
所有的代码都写成这样,会非常尴尬,因为,我们必须显式复制存储在记录中的所有字段值。此外,最终可能需要添加新字段来记录声明,这可能打乱现有的所有代码。F# 让我们以简洁的方式来表达这样的思想,"复制现有的记录,并作一些修改":
let rc2 = { rc with Left = rc.Left + 100.0f }
使用 with 关键字,我们可以指定要更改的字段值,而所有其余字段会自动复制。这与前面的代码具有相同的含义,但它要实际得多。
到目前为止,我们已经看到如何写记录的"原始"操作。当然,我们试图以函数风格写代码,所以,我们真正想能够用函数来操作记录。
处理记录
我们将在本章后面使用 Rect 类型,需要两个简单的函数,来处理矩形。第一个函数是通过从每一条边上减去指定的宽度和高度,来缩小矩形,第二个是将我们的表示形式转换成 System.Drawing 命名空间中的 RectangleF 类。可以在清单 7.2 中看到两个函数。
Listing 7.2 Functions for working with rectangles (F# Interactive)
> open System.Drawing;;
> let deflate(original, wspace, hspace) =
{ Left = original.Left + wspace
Top = original.Top + hspace
Width = original.Width - (2.0f * wspace)
Height = original.Height - (2.0f * hspace) };;
val deflate : Rect * float32 * float32 –> Rect
> let toRectangleF(original) =
RectangleF(original.Left, original.Top,
original.Width, original.Height);;
val toRectangleF : Rect –> RectangleF
> { Left = 0.0f; Top = 0.0f;
Width = 100.0f; Height = 100.0f; };;
val it : Rectangle = (...)
> deflate(it, 20.0f, 10.0f);;
val it : Rectangle = { Left = 20.0f; Top = 10.0f;
Width = 60.0f; Height = 80.0f;}
从类型签名可以看到,F# 编译器正确推导出原始参数的类型是 Rect 类型,编译器使用在函数体中访问的字段名。如果我们有两个记录类型,并且仅使用由它们两个共享的字段,就必须显式指定其类型。我们可以使用类型注释,在函数声明中写上 (original:Rect)。通常,当使用 F# Interactive 时,可以立即测试函数。当创建值时,没我们有使用 let 绑定,所以,后面使用自动创建的值 it 来访问。
总结一下,F# 的记录是不可变的,可以使用 {x with ...} 构造很容易地复制。如果我们在 C# 中,设计像这样的数据结构,要使用类,偶尔会用到结构,用特殊的方法写。在下一节中,我们将介绍如何做到这一点。