[英]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.