簡體   English   中英

F#中的不可變值

[英]Immutable values in F#

我剛剛開始使用F#,並且有一個基本問題。

這是代碼:

let rec forLoop body times =
    if times <= 0 then
        ()
    else
        body()
        forLoop body (times - 1)

我不知道如何在定義變量時將其定義為值並且是不可變的。 在這里,值不斷變化以便循環。 這與C#中的變量有何不同?

它沒有改變。 您使用遞歸。 變量保持不變,但是將其減去1並傳遞給函數。 在這種情況下,功能是相同的。

堆棧看起來像

forLoop body 0
 |
 forLoop body 1
   |
   forLoop body 2

呈現的代碼不會在C#中表示為for循環,而是遞歸的(類似這樣):

void ForLoop(int times, Action body)
{
  if (times <= 0)
  {
     return;
  }
  else
  {
     body();
     ForLoop(times - 1, body);
  }
}

如您所見,值times在任何times都不會改變。

每個遞歸調用中每個times實例都是內存中的一個不同對象。 如果body()以任何方式使用times ,它將從當前堆棧幀中捕獲不可變值,該值與后續遞歸調用中的值不同。

下面是C#和F#程序,它們顯示出差異可能很重要的一種方法。

C#程序-打印一些隨機數:

using System;
using System.Threading;

class Program
{
    static void ForLoop(int n)
    {
        while (n >= 0)
        {
            if (n == 100)
            {
                ThreadPool.QueueUserWorkItem((_) => { Console.WriteLine(n); });
            }
            n--;
        }
    }
    static void Main(string[] args)
    {
        ForLoop(200);
        Thread.Sleep(2000);
    }
}

F#程序-始終打印100:

open System
open System.Threading 
let rec forLoop times = 
    if times <= 0 then 
        () 
    else 
        if times = 100 then
            ThreadPool.QueueUserWorkItem(fun _ -> 
                Console.WriteLine(times)) |> ignore
        forLoop (times - 1) 

forLoop 200
Thread.Sleep(2000)

之所以會出現這種差異,是因為在C#代碼中傳遞給QueueUserWorkItem的lambda捕獲了一個可變變量,而在F#版本中,它捕獲了一個不可變值。

當執行調用(任何調用)時,運行時將分配一個新的堆棧框架,並在新的堆棧框架中存儲被調用函數的參數和局部變量。 當您執行遞歸調用時,分配的幀包含具有相同名稱的變量,但是這些變量存儲在不同的堆棧幀中。

為了說明這一點,我將使用示例的稍微簡化的版本:

let rec forLoop n = 
  if times > 0 then 
    printf "current %d" n
    forLoop body (n - 1) 

現在,假設我們從程序的某些頂級函數或模塊調用forLoop 2 運行時為調用分配堆棧,並將參數的值存儲在表示forLoop調用的幀中:

+----------------------+
| forLoop with n = 2   |
+----------------------+
| program              |
+----------------------+

forLoop函數打印2並繼續運行。 它對forLoop 1執行遞歸調用,該調用分配一個新的堆棧幀:

+----------------------+
| forLoop with n = 1   |
+----------------------+
| forLoop with n = 2   |
+----------------------+
| program              |
+----------------------+

由於1 > 0 ,程序再次進入then分支,打印1並再次遞歸調用forLoop函數:

+----------------------+
| forLoop with n = 0   |
+----------------------+
| forLoop with n = 1   |
+----------------------+
| forLoop with n = 2   |
+----------------------+
| program              |
+----------------------+

此時, forLoop函數不進行任何其他調用就返回,並且隨着程序從所有遞歸調用中返回,堆棧幀被一一刪除。 從圖中可以看到,我們創建了三個不同的變量,這些變量存儲在不同的堆棧框架中(但它們都被命名為n )。

還值得注意的是,F#編譯器執行了各種優化,例如tail-call ,它可以使用可變變量來替換調用和分配新的堆棧幀(更有效)。 但是,這只是一種優化,如果您想了解遞歸的心理模型,則無需擔心。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM