首页 > 代码库 > 9.1.1 添加成员到 F# 类型

9.1.1 添加成员到 F# 类型

9.1.1 添加成员到 F# 类型

 

现在,F# 迭代式开发就派上用场了。交互式调试和测试代码的能力,在开发的早期阶段更重要;随着代码更加完善,就要考虑与其他开发人员共享项目,把通用的操作提供作为成员,可以使用点表示法来调用,变得更加重要。

这就是说,在 F# 中,把数据类型与操作封装一起,通常是开发过程的最后一步。这可以使用成员(members)来完成,可以添加到任何 F# 类型中,行为就像 C# 的方法或属性。清单 9.2 显示了如何增加两个使用成员操作的 Rect 类型。

 

清单 9.2 操作作为成员的Rect 类型(F#)

type Rect =     [1]

  { Left : float32 

    Top : float32 

    Width : float32 

    Height : float32 }

 

/// Creates a rectangle which is deflatedby ‘wspace‘ from the  | [2]

/// left and right and by ‘hspace‘ from thetop and bottom     |

member x.Deflate(wspace, hspace) =     [3]

  { Left = x.Left + wspace 

    Top = x.Top +hspace 

    Width = x.Width - (2.0f* wspace) 

    Height = x.Height -(2.0f * hspace) }

 

/// Converts the rectangle torepresentation from ‘System.Drawing‘ 

member x.ToRectangleF() = 

  RectangleF(x.Left, x.Top, x.Width,x.Height)

 

要创建有成员的F# 数据类型,成员声明放在正常的 F# 类型声明的后面。正如在这个示例中看到的,成员声明的缩进要与类型声明的主体有同样多的空格。在清单 9.2 中,首先是正常的 F# 记录类型的声明[1],然后,添加两个不同的方法作为成员。

成员声明以关键字 member 开始,后面跟的是成员名字,带有表示当前实例的值的名字。例如,x.Deflate 表示要声明方法 Deflate,在成员体的内部,值 x 指向记录的当前实例,这有点类似与于 C# 的 this 关键字,因此,可以考虑把 this 改成你喜欢的任何名字。F# 没有保留 this 作为关键字,因此,可以把值命名为this(示例中可以写成 this.Deflate),然后,就使用与 C# 中一样的关键字表示当前实例。在本书中,我们主要使用名字 x,出于简洁的目的,F# 的社区都这样使用。除了 this 和 x 之外,另一个名字 self 也不是 F# 的关键字。

第一个成员[3]的参数为元组,通过在水平和垂直两个方向,减去指定的长度,创建一个更小的矩形。创建带成员的类型,F# 的开发人员通常把成员的参数声明为元组。这样做,可以编译成适合从 C# 使用的标准方法;如果指定参数不带括号,可以使用标准的函数技术,比如,带成员的散函数应用。

示例中另一个值得注意的是成员前面的注释[2],现在以三个正斜杠(/)开始,这种注释表示成员的文档,类似于 C# 的 XML 注释。在 F# 中,如果愿意,可以使用类似于 XML 的语法,但是,如果写平面的非 XML 文本,注释自动视为摘要。

现在,我们看一下如何使用声明的成员。选中代码以后,在 F# Interactive 中运行,就会看到 Rect 类型的类型签名,其中包括成员和类型。清单 9.3 演示了如何调用两个成员。

 

清单 9.3 使用类型和成员 (F# Interactive)

> let rc = { Left = 0.0f; Top =0.0f             | 创建 Rect 值

                Width = 100.0f; Height = 100.0f };;   |

val rc : Rect

 

> let small = rc.Deflate(10.0f,30.0f);;       [1]

val small : Rect = { Left = 10.0f; Top =30.0f 

                           Width = 80.0f; Height = 40.0f }

 

> small.ToRectangleF();;       [2]

val rcf : RectangleF = {X=10, Y=30,Width=80, Height=40} { ... }

 

我们首先创建 Rect 类型的值。这并没有改变,我们仍然为记录类型的每个属性指定一个值。下一个命令[1]调用 Deflate 成员。可以看到,我们使用了标准的面向对象的点符号,这在处理 .NET 对象时用过。在这里,使用元组描述参数值,但是如果我们在声明时,描述参数没有用括号的话,调用也可以使用 F#函数调用语法,即,参数用空格隔开。最后一个命令[2]是把矩形转换成 System.Drawing 的 RectangleF 对象的值。这个示例现在看起来非常像面向对象代码,但是,在任意意义上,都不能说明我们想摆脱函数式编程风格。

 

注意

 

这段代码仍然是纯函数式的(与命令式相对),就是说,尽管它更像面向对象的组织,但没有副作用。如果以命令风格实现,调用Deflate 方法可能会修改矩形的属性;但我们的实现不是这样做的,Rect 数据类型仍然是不可变的:实例一旦创建,属性值就不能改变。因此,成员不是修改这个值,而是返回属性修改后的新Rect 值。这与原始的 deflate 函数的行为相同,但是,要牢记,很好地结合函数概念(如不变性)与面向对象的概念(在这里,封装)非常重要。当然,这对于命令式的面向对象编程,也是不陌生的概念,可以看一下 System.String 类型,它就采用了同样的做法。

 

我们已经提到过,使用成员而不是函数有一个好处,即,可以轻松地利用智能感知发现处理值的操作。在图 9.1 中,可以看到 Visual Studio 编辑器处理 Rect 类型。


图9.1在Visual Studio IDE 中编辑 F# 源代码时,会显示 Rect 类型成员的提示

 

另一个重要的好处是,带成员的类型很自然地可用于其他 .NET 语言,比如 C#。如果我们在 C# 中使用Deflate 成员,它看起来就像类型的普通方法,我们将在 9.5 节看到。为了使所有成员与 C# 兼容,这总是有可能的,必须精心设计成员。有一些棘手的情况将在抨击讨论,比如 9.5 节的高阶函数,和第16章的事件。

当我们在清单 9.2 中把函数转变到成员时,也把使用 let 绑定声明的函数,转换到使用关键字 member 声明的成员。这虽然能够运行,但是必须对源代码做大的修改。幸运的是,我们能够避免这种情况,从简单的 F# 风格平滑过渡到更地道的 .NET 风格。

 

9.1.1 添加成员到 F# 类型