而对于F#的初学者,或是C#和F#混用的程序员来说,我认为F#中最容易令人混乱的命令是Reference Cells的取值操作了。下面便详细谈谈这么说的原因,及建议的应对办法。
F#是一门函数式编程语言,函数式编程语言的特点之一便是No Side Effect(无副作用),Immutable(不可变)。但是在很多场景下,Mutable(可变)可以给我带来很多便利,尤其是在结合命令式编程的场景中。因此F#提供了将某个“标识符”定义为“可变”的方式,主要有两种:使用mutable关键字或是Reference Cells。
在大部分情况下,我推荐(微软也这么推荐的)使用mutable关键字,因为这样标识符在使用上也已经和普通变量没有任何区别了。与之相对,使用Reference Cell进行读写操作都需要一些特殊的操作/指令。不过的确有一些场景必须使用Reference Cells,您可以关注MSDN上的说明。例如,在mutable的标识符在读取和赋值时,和普通的属性没有什么区别:
- let mutable a = 0
- a <- 1 // assign mutable variable
- let request = WebRequest.Create("http://www.51cto.com")
- request.ContentType <- "text/xml" // assign property
但是对于Reference Cells来说,它的读取和写入就需要使用!与:=操作符了:
- let a = ref 0
- a := 1 // assign value
- printfn "%i" !a // retrieve value
这个感叹号便是引起混乱的源泉,且看以下代码:
- let transfer (streamIn: Stream) (streamOut: Stream) buffer =
- let hasData = ref true
- while !hasData do
- let lengthRead = streamIn.Read(buffer, 0, buffer.Length)
- if lengthRead > 0 then
- streamOut.Write(buffer, 0, lengthRead)
- else
- hasData := false
上面的代码定义了一个transfer函数,将一个数据流中的数据全部传输到另一个数据流中。在这里我们使用了命令式的编程方式,并使用一个名为hasData的Ref Cell来表明是否读完了数据。
不过,您看到while语句中的!hasData是什么感觉?至少对于我这样混写C#和F#的人来说,我的***反应是“嗯,取反?”,然后才是“哦,只是Ref Cells的取值操作”。对于其他一些场景下可能这点不会成为问题,但如果这个Ref Cell是个布尔值,然后又放在if或while的时候,混乱就这样开始了。因此,我目前可能会倾向于使用这样的方式:
- let transfer (streamIn: Stream) (streamOut: Stream) buffer =
- let hasData = ref true
- while hasData.Value do
- let lengthRead = streamIn.Read(buffer, 0, buffer.Length)
- if lengthRead > 0 then
- streamOut.Write(buffer, 0, lengthRead)
- else
- hasData := false
在F#中,一个Ref Cell其实是一个Ref<'a>类型的对象,它有一个'a类型(泛型类型)的Value属性,可读写。因此,如果我们在上面的代码中直接使用Value属性,那么我想就不会让任何人混乱了。当然,***的办法可能还是写一些immutable的代码吧,例如:
- let rec transfer (streamIn: Stream) (streamOut: Stream) buffer =
- let lengthRead = streamIn.Read(buffer, 0, buffer.Length)
- if lengthRead > 0 then
- streamOut.Write(buffer, 0, lengthRead)
- transfer streamIn streamOut buffer
对于F#来说,这样的(尾)递归和之前的实现方式可以说是完全等价的。
【编辑推荐】