繁体   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