跳转到内容

F# 编程/可变数据

来自维基教科书,为开放世界提供开放书籍
前一个:区分联合 索引 下一个:控制流
F# : 可变数据

到目前为止,F# 中看到的所有数据类型和值都是不可变的,这意味着这些值在声明后无法重新分配另一个值。但是,F# 允许程序员创建真正意义上的变量:值可以在应用程序的整个生命周期内发生变化的变量。

mutable 关键字

[编辑 | 编辑源代码]

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 单元格克服了可变变量的一些局限性。事实上,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 单元格的别名

[编辑 | 编辑源代码]
注意:虽然命令式编程广泛使用别名,但这种做法存在一些问题。特别是,它使程序难以理解,因为任何变量的状态都可以在应用程序中的任何地方随时修改。此外,共享可变状态的多线程应用程序难以推理,因为一个线程可能会更改另一个线程中变量的状态,这会导致与竞争条件和死锁相关的许多细微错误。

Ref 单元格与 C 或 C++ 指针非常相似。可以将两个或多个 ref 单元格指向同一个内存地址;对该内存地址的更改将更改指向它的所有 ref 单元格的状态。从概念上讲,这个过程看起来像这样

假设有 3 个 ref 单元格指向内存中的同一个地址

Three references to an integer with value 7

cell1cell2cell3 都指向内存中的同一个地址。每个单元格的 .contents 属性为 7。假设在程序中的某个时候,执行代码 cell1 := 10,这会将内存中的值更改为以下内容

Three references to an integer with value 10

通过为 cell1.contents 分配一个新值,变量 cell2cell3 也发生了更改。这可以使用 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
前一个:区分联合 索引 下一个:控制流
华夏公益教科书