跳轉到內容

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
前一個:區分聯合 索引 下一個:控制流
華夏公益教科書