在上一节我们提到过,可以为任何 F# 数据类型添加成员;现在,我们将使用差别联合来演示。这种种方法能够添加成员,而不需要修改任何原始代码。这样,我们将能够保留原始类型和原始的函数声明,不作修改,然后添加成员。
我们将扩展第五章声明 schedule 类型的示例,这个类型表示的事件可以只发生一次,或重复发生,或从不发生。除了数据类型之外,我们还创建了计算事件下一次发生时间的函数。清单 9.4 是代码稍作修改后的版本,我们使代码更紧凑,并使用简单的工具函数,重构了模式匹配中的 Once 分支;如果想比较的话,原始代码在清单 5.5 中。
清单 9.4 有函数的Schedule 数据类型 (F#)
type Schedule = [1] <-- 声明原始类型
| Never
| Once of DateTime
| Repeatedly of DateTime * TimeSpan
let futureOrMaxValue(dt) = [2] <-- 实现工具函数
if (dt > DateTime.Now) then dtelse DateTime.MaxValue
let getNextOccurrence(schedule) = [3] <-- 指定公开的行为
match schedule with
| Never ->DateTime.MaxValue
| Once(eventDate) ->futureOrMaxValue(eventDate)
| Repeatedly(startDate, interval)–>
let secondsFromFirst =(DateTime.Now - startDate).TotalSeconds
let q = max(secondsFromFirst / interval.TotalSeconds) 0.0
startDate.AddSeconds
(interval.TotalSeconds * (Math.Floor(q) + 1.0))
最重要的变化是,我们增加了工具函数 futureOrMaxValue[2]。这个改变并不显著提高可读性,只是用来说明有这种选择。在一个更复杂的项目中,肯定会有一些工具函数。
这个观点就是,在典型的 F# 源文件中,首先声明类型,然后,有一堆工具(私有)函数,再后是我们想要公开为成员的函数[3]。如果我们想利用上一节介绍的方法,把最后的函数变为成员,这是相当困难的。因为成员必须是类型声明的一部分,但是,我们通常想把工具函数放在类型和其成员之间!
要解决这个问题,需要使用固有类型扩展(intrinsic type extensions),它能够为在文件中先声明的类型,添加成员。清单 9.5 显示了我们如何能够为schedule 类型使用扩展。
清单 9.5 利用固有类型扩展添加成员 (F#)
type Schedule =
| Never
| Once of DateTime
| Repeatedly of DateTime *TimeSpan
let futureOrMaxValue(dt) =
(...)
let getNextOccurrence(schedule) = [1]
(...)
type Schedule with [2]
member x.GetNextOccurrence() =getNextOccurrence(x) [3]
member x.OccursNextWeek = | [4]
getNextOccurrence(x)< DateTime.Now.AddDays(7.0) |
对比清单 9.4 的代码,大部分没有改变,为了简洁,所以就省略了;唯一增加的是最后四行代码。第一行[2]定义了类型扩展,告诉 F# 编译器,要把后面的成员添加到指定名字的类型;后面就是正常的成员声明。因为我们已经把核心功能实现为函数[1],成员的实现就简单了[3]。除了得到下一次发生的时间以外,我们还增加了一个属性[4],使用私有函数检查下一次发生的时间是否就在接下来的一周。
如果学过 C# 3.0,就会发现类型扩展和扩展方法之间有相似性。使用类型扩展,可以为其他程序集中现有类型添加方法和属性。前一个清单的情况有所不同,因为我们使用了固有类型扩展。这是一种特殊的情况,即,声明原始类型和扩展在同一个文件中;在这种情况下,F# 编译器会把类型的两部分合并到同一个类中,我们也能访问在类型扩展中类型的私有成员。
清单 9.6 演示了调用清单 9.5 中的成员。使用类型扩展添加的成员,其行为与其他成员一样的,因此,清单没有任何意外惊喜。
清单 9.6 使用成员处理 Schedule (F# Interactive)
> let sch = Repeatedly(DateTime.Now,TimeSpan(2, 0, 0, 0));;
val sch : Schedule
> sch.OccursNextWeek();; <-- 使用重复事件进行测试
val it : bool = true
> let sch = Never;;
val sched : Schedule
> sch.OccursNextWeek();; <-- 测试无计划事件的行为
val it : bool = false
正如我们处理记录时,通常的方法是创建 F# 值;对于在我们示例中的差别联合,就是使用 Repeatedly 或 Never 识别器(我们还可以使用 Once 识别器)。我们有了值以后,就可以使用面向对象的点符号调用它的成员。
正如我们刚看到的,在写成熟代码时,成员是非常有用的,因为它可以把代码包装成结构良好的片断,以方便使用类型。在 F# 的开发过程中,我们通常不首先写有成员的代码,只有代码测试通过以后,才添加成员,API 设计就固定了。我们已经讨论过添加成员的两种方法:
■类型足够简单时,直接在类型声明中追加成员。
■对于复杂的类型,使用固有类型扩展,可以少改变代码。
类型扩展还有一个好处,可以在扩充之前,使用 F# Interactive 工具,测试类型和它的处理函数,因为我们不必一口气声明整个类型。
我们已经知道了,对于把以数据为中心的 F# 代码转变成实际的 .NET 应用程序或组件,成员是非常重要的。现在,我们将把注意力转向以行为中心的应用程序。