首页 > 代码库 > 9.4.1 函数式和命令式类
9.4.1 函数式和命令式类
9.4.1 函数式和命令式类
在类或者类的构造函数的参数值中的 let 绑定,就像我们在其他 F# 代码中看到的 let 绑定一样,也是不可变值;此外,使用 member 关键字声明的属性,创建的是只读属性(只有 getter)。因此,如果类只引用其他不可变类型的值,那么,类也不可变。
比方说,在前面的例子中,我们要允许改变客户的收入,可以有两种方式:
■以纯函数式风格,对象将返回一个新实例,收入是更新后的,其他所有属性都是原始值。
■使用命令式风格,收入将是可变字段。
清单 9.15 展示了类的函数式版本(ClientF),以及命令式的类,名为ClassI。
清单 9.15 Client 类型的函数式和命令式版本 (F#)
type ClientF(name, inc) = member x.Name = name | [1] member x.Income = inc |
member x.WithIncome(v) = [2] new ClientF(name, v)
member x.Report() = printfn "%s %d" name inc | type ClientI(name, inc) = let mutable inc = inc [3]
member x.Name = name member x.Income with get() = inc | [4] and set(v) = inc <- v |
member x.Report() = printfn "%s %d" name inc |
在函数式版本中,所有属性都是只读的[1]。s 改变客户的收入,必须创建客户的一个新实例,使用 WithIncome 方法[2]很容易做到,它返回对象的副本,收入为新设置的值。
命令式版本使用 mutable 关键字[3],声明了可更新的字段,保存收入。声明字段,我们使用的名字,与值和构造函数的参数相同;新的值隐藏了原来的值,这样,我们再也不能访问原始值了。初看起来,这可能很奇怪,但是,可以防止原打算使用当前值(可能已经更改)时,而意外使用了初始值。
在命令式版本中,另一个值得注意的是用来改变客户收入的读/写属性,这个属性是由两个成员组成,类似于方法声明。get 成员没有任何参数,只返回值;而 set 成员只有一个参数,表示新值的,返回结果为 unit。虽然这个语法与 C# 属性声明略有不同,但原理相同。
当改变客户收入时,两个类的用法不同,但是,两个版本得到的结果相同。可以在清单 9.16 中看到 F# Interactive 会话,演示如何使用这两个类。
清单 9.16 Client 类型的函数式和命令式编程 (F# Interactive)
> let joeOriginal = newClientF("Joe", 30);;
val joeOriginal : ClientF
> let joeUpdated =joeOriginal.WithIncome(40);; [1]<-- 创建客户的新实例
val joeUpdated : ClientF
> joeUpdated.Income;;
val it : int = 40
> let joeMutable = newClientI("Joe", 30);;
val joeMutable : ClientI
> joeMutable.Income <- 40;; [2] <-- 修改已有的实例
val it : unit = ()
> joeMutable.Income;;
val it : int = 40
当使用不可变的版本时[1],我们把返回的客户值,用新的名字来保存。这样,我们仍然可以访问原始值。如果在后面的代码中,我们不想访问原始实例,就可以使用值隐藏,两个值使用相同的名字。在命令式版本中,可以使用读/写属性[2],改变收入。我们使用 <- 运算符,就像在标准的 .NET 库中使用其他声明的对象一样。
虽然我们重点在于函数式编程,但是,知道如何写这样的可变类,有时也是有用的。如果需要公开大段的 F# 代码给 C# 客户端,可能要把代码包装到至少一个类中,因为,这样更方便从 C# 中使用。在这一点上,可以随便选择一种风格:命令式风格,有一些可变类型;纯函数式风格,所有都是可变的。从 F# 的观点来看,第二种方法是更清晰的;但对于开发人员来说,不习惯于处理完全组合不可变类型的库,可能会发现使用可变状态的包装会更容易。
我们几乎准备展示从 C# 调用 F# 代码的完整示例子了,但还需要先完成面向对象的 F# 功能之旅。
9.4.1 函数式和命令式类