F# 编程/可变数据
F# : 可变数据 |
到目前为止,F# 中看到的所有数据类型和值都是不可变的,这意味着这些值在声明后无法重新分配另一个值。但是,F# 允许程序员创建真正意义上的变量:值可以在应用程序的整个生命周期内发生变化的变量。
F# 中最简单的可变变量是使用 mutable
关键字声明的。以下是使用 fsi 的示例
> let mutable x = 5;;
val mutable x : int
> x;;
val it : int = 5
> x <- 10;;
val it : unit = ()
> x;;
val it : int = 10
如上所示,<-
运算符用于为可变变量分配一个新值。请注意,变量分配实际上返回 unit
作为值。
mutable
关键字经常与记录类型一起使用以创建可变记录
open System
type transactionItem =
{ ID : int;
mutable IsProcessed : bool;
mutable ProcessedText : string; }
let getItem id =
{ ID = id;
IsProcessed = false;
ProcessedText = null; }
let processItems (items : transactionItem list) =
items |> List.iter(fun item ->
item.IsProcessed <- true
item.ProcessedText <- sprintf "Processed %s" (DateTime.Now.ToString("hh:mm:ss"))
Threading.Thread.Sleep(1000) (* Putting thread to sleep for 1 second to simulate
processing overhead. *)
)
let printItems (items : transactionItem list) =
items |> List.iter (fun x -> printfn "%A" x)
let main() =
let items = List.init 5 getItem
printfn "Before process:"
printItems items
printfn "After process:"
processItems items
printItems items
Console.ReadKey(true) |> ignore
main()
Before process: {ID = 0; IsProcessed = false; ProcessedText = null;} {ID = 1; IsProcessed = false; ProcessedText = null;} {ID = 2; IsProcessed = false; ProcessedText = null;} {ID = 3; IsProcessed = false; ProcessedText = null;} {ID = 4; IsProcessed = false; ProcessedText = null;} After process: {ID = 0; IsProcessed = true; ProcessedText = "Processed 10:00:31";} {ID = 1; IsProcessed = true; ProcessedText = "Processed 10:00:32";} {ID = 2; IsProcessed = true; ProcessedText = "Processed 10:00:33";} {ID = 3; IsProcessed = true; ProcessedText = "Processed 10:00:34";} {ID = 4; IsProcessed = true; ProcessedText = "Processed 10:00:35";}
可变变量有一些局限性:在 F# 4.0 之前,可变变量在定义它们的函数作用域之外是无法访问的。具体来说,这意味着无法在另一个函数的子函数中引用可变变量。以下是 fsi 中的演示
> let testMutable() =
let mutable msg = "hello"
printfn "%s" msg
let setMsg() =
msg <- "world"
setMsg()
printfn "%s" msg;;
msg <- "world"
--------^^^^^^^^^^^^^^^
stdin(18,9): error FS0191: The mutable variable 'msg' is used in an invalid way. Mutable
variables may not be captured by closures. Consider eliminating this use of mutation or
using a heap-allocated mutable reference cell via 'ref' and '!'.
Ref 单元格克服了可变变量的一些局限性。事实上,ref 单元格是非常简单的 datatype,它们将可变字段封装在记录类型中。Ref 单元格由 F# 定义如下
type 'a ref = { mutable contents : 'a }
F# 库包含用于处理 ref 单元格的几个内置函数和运算符
let ref v = { contents = v } (* val ref : 'a -> 'a ref *)
let (!) r = r.contents (* val (!) : 'a ref -> 'a *)
let (:=) r v = r.contents <- v (* val (:=) : 'a ref -> 'a -> unit *)
ref
函数用于创建 ref 单元格,!
运算符用于读取 ref 单元格的内容,:=
运算符用于为 ref 单元格分配一个新值。以下是在 fsi 中的示例
> let x = ref "hello";;
val x : string ref
> x;; (* returns ref instance *)
val it : string ref = {contents = "hello";}
> !x;; (* returns x.contents *)
val it : string = "hello"
> x := "world";; (* updates x.contents with a new value *)
val it : unit = ()
> !x;; (* returns x.contents *)
val it : string = "world"
由于 ref 单元格在堆上分配,因此它们可以在多个函数之间共享
open System
let withSideEffects x =
x := "assigned from withSideEffects function"
let refTest() =
let msg = ref "hello"
printfn "%s" !msg
let setMsg() =
msg := "world"
setMsg()
printfn "%s" !msg
withSideEffects msg
printfn "%s" !msg
let main() =
refTest()
Console.ReadKey(true) |> ignore
main()
withSideEffects
函数的类型为 val withSideEffects : string ref -> unit
。
该程序输出以下内容
hello world assigned from withSideEffects function
withSideEffects
函数之所以这样命名,是因为它具有 *副作用*,这意味着它可以改变其他函数中变量的状态。Ref 单元格应该像火一样对待。在绝对必要时谨慎使用,但在一般情况下避免使用。如果您在将代码从 C/C++ 翻译时发现自己使用 Ref 单元格,那么先忽略效率,看看是否可以在没有 Ref 单元格的情况下完成,或者最多使用 mutable。您通常会偶然发现一个更优雅、更易于维护的算法
- 注意:虽然命令式编程广泛使用别名,但这种做法存在一些问题。特别是,它使程序难以理解,因为任何变量的状态都可以在应用程序中的任何地方随时修改。此外,共享可变状态的多线程应用程序难以推理,因为一个线程可能会更改另一个线程中变量的状态,这会导致与竞争条件和死锁相关的许多细微错误。
Ref 单元格与 C 或 C++ 指针非常相似。可以将两个或多个 ref 单元格指向同一个内存地址;对该内存地址的更改将更改指向它的所有 ref 单元格的状态。从概念上讲,这个过程看起来像这样
假设有 3 个 ref 单元格指向内存中的同一个地址
cell1
、cell2
和 cell3
都指向内存中的同一个地址。每个单元格的 .contents
属性为 7
。假设在程序中的某个时候,执行代码 cell1 := 10
,这会将内存中的值更改为以下内容
通过为 cell1.contents
分配一个新值,变量 cell2
和 cell3
也发生了更改。这可以使用 fsi 演示如下
> let cell1 = ref 7;;
val cell1 : int ref
> let cell2 = cell1;;
val cell2 : int ref
> let cell3 = cell2;;
val cell3 : int ref
> !cell1;;
val it : int = 7
> !cell2;;
val it : int = 7
> !cell3;;
val it : int = 7
> cell1 := 10;;
val it : unit = ()
> !cell1;;
val it : int = 10
> !cell2;;
val it : int = 10
> !cell3;;
val it : int = 10
F# 不鼓励在函数之间传递可变数据的做法。依赖于突变的函数通常应该在其私有函数后面隐藏其实现细节,例如以下 FSI 中的示例
> let incr =
let counter = ref 0
fun () ->
counter := !counter + 1
!counter;;
val incr : (unit -> int)
> incr();;
val it : int = 1
> incr();;
val it : int = 2
> incr();;
val it : int = 3