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