F#中的不可变值

时间:2010-04-12 17:22:25

标签: f#

我刚开始使用F#并有一个基本问题。

以下是代码:

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

我没有得到如何定义变量它是一个值和不可变的概念。这里,值正在变化以循环。这与C#中的变量有什么不同?

4 个答案:

答案 0 :(得分:5)

它没有改变。你使用递归。 变量保持不变,但会减去一个并传递给函数。在这种情况下,功能是相同的。

堆栈看起来像

forLoop body 0
 |
 forLoop body 1
   |
   forLoop body 2

答案 1 :(得分:5)

所呈现的代码不会在C#中表示为for循环,它将是递归的(类似这样):

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

如您所见,times值在任何时候都不会改变。

答案 2 :(得分:1)

每个递归调用中的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#版本中它捕获了一个不可变的值。

答案 3 :(得分:1)

当您执行调用(任何调用)时,运行时会分配一个新的堆栈帧,并将被调用函数的参数和局部变量存储在新的堆栈帧中。执行递归调用时,分配的帧包含具有相同名称的变量,但这些变量存储在不同的堆栈帧中。

为了证明这一点,我将使用您的示例的略微简化版本:

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 ,它可以使用可变变量替换调用和新堆栈帧的分配(这是更高效)。但是,这只是一个优化,如果您想了解递归的心智模型,则无需担心。