简体   繁体   English

F#中的不可变值

[英]Immutable values in F#

I'm just getting started in F# and have a basic question. 我刚刚开始使用F#,并且有一个基本问题。

Here's the code: 这是代码:

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

I don't get the concept of how when you define a variable it is a value and immutable. 我不知道如何在定义变量时将其定义为值并且是不可变的。 Here, the value is changing in order to loop. 在这里,值不断变化以便循环。 How is that different from a variable in C#? 这与C#中的变量有何不同?

it is not changing. 它没有改变。 you use recursion. 您使用递归。 that variable remains unchanged, but it is subtracted one and passed to function. 变量保持不变,但是将其减去1并传递给函数。 function is same in this case. 在这种情况下,功能是相同的。

stack will look like 堆栈看起来像

forLoop body 0
 |
 forLoop body 1
   |
   forLoop body 2

The code presented would not be represented as a for loop in C#, it would be recursive (something like this): 呈现的代码不会在C#中表示为for循环,而是递归的(类似这样):

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

As you can see, the value times is not changed at any point. 如您所见,值times在任何times都不会改变。

Each instance of times in each recursive call is a different object in memory. 每个递归调用中每个times实例都是内存中的一个不同对象。 If body() uses times in any way, it captures the immutable value from the current stack frame, which is different from values in subsequent recursive calls. 如果body()以任何方式使用times ,它将从当前堆栈帧中捕获不可变值,该值与后续递归调用中的值不同。

Below are a C# and F# program that show one way the difference may be important. 下面是C#和F#程序,它们显示出差异可能很重要的一种方法。

C# program - prints some random number: 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# program - always prints 100: 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)

The differences arise because the lambda passed to QueueUserWorkItem in the C# code captures a mutable variable, whereas in the F# version it captures an immutable value. 之所以会出现这种差异,是因为在C#代码中传递给QueueUserWorkItem的lambda捕获了一个可变变量,而在F#版本中,它捕获了一个不可变值。

When you perform a call (any call), the runtime allocates a new stack frame and stores parameters and local variables of the called function in the new stack frame. 当执行调用(任何调用)时,运行时将分配一个新的堆栈框架,并在新的堆栈框架中存储被调用函数的参数和局部变量。 When you perform a recursive call, the allocated frames contain variables with the same names, but these are stored in different stack frames. 当您执行递归调用时,分配的帧包含具有相同名称的变量,但是这些变量存储在不同的堆栈帧中。

To demonstrate this, I'll use a slightly simplified version of your example: 为了说明这一点,我将使用示例的稍微简化的版本:

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

Now, let's say that we call forLoop 2 from some top-level function or module of a program. 现在,假设我们从程序的某些顶级函数或模块调用forLoop 2 The runtime allocates the stack for the call and stores the value of the parameter in the frame representing the forLoop call: 运行时为调用分配堆栈,并将参数的值存储在表示forLoop调用的帧中:

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

The forLoop function prints 2 and continues running. forLoop函数打印2并继续运行。 It performs a recursive call to forLoop 1 , which allocates a new stack frame: 它对forLoop 1执行递归调用,该调用分配一个新的堆栈帧:

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

Since 1 > 0 the program enters the then branch once again, prints 1 and makes one more recursive call to the forLoop function: 由于1 > 0 ,程序再次进入then分支,打印1并再次递归调用forLoop函数:

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

At this point, the forLoop function returns without making any other calls and stack frames are removed one by one as the programs returns from all the recursive calls. 此时, forLoop函数不进行任何其他调用就返回,并且随着程序从所有递归调用中返回,堆栈帧被一一删除。 As you can see from the diagrams, we created three different variables that were stored on different stack frames (but all of them were named n ). 从图中可以看到,我们创建了三个不同的变量,这些变量存储在不同的堆栈框架中(但它们都被命名为n )。

It is also worth noting that the F# compiler performs various optimizations such as tail-call , which can replace a call and an allocation of a new stack frame with a use of mutable variable (which is more efficient). 还值得注意的是,F#编译器执行了各种优化,例如tail-call ,它可以使用可变变量来替换调用和分配新的堆栈帧(更有效)。 However, this is just an optimization and you don't need to worry about that if you want to understand the mental model of recursion. 但是,这只是一种优化,如果您想了解递归的心理模型,则无需担心。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM