我刚开始使用F#并有一个基本问题。
以下是代码:
let rec forLoop body times =
if times <= 0 then
()
else
body()
forLoop body (times - 1)
我没有得到如何定义变量它是一个值和不可变的概念。这里,值正在变化以循环。这与C#中的变量有什么不同?
答案 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 ,它可以使用可变变量替换调用和新堆栈帧的分配(这是更高效)。但是,这只是一个优化,如果您想了解递归的心智模型,则无需担心。