执行捕获变量的嵌套函数分配

Yai*_*adt 1 closures scala nested-function

scala 中的嵌套函数可以捕获父函数中的变量。

例如

def outer = {
    var a = 0
    def inner = {
        a = 42
    }
    inner()
    a
}
Run Code Online (Sandbox Code Playgroud)

在 C# 中,这是通过将所有捕获的变量存储在一个结构体上并在 byref 中传递该结构体来实现的。这避免了嵌套函数分配,除非您将其转换为函数对象。在sharplab中看到这个例子

然而,在 scala 中,您不能通过 ref 传递变量,因此唯一可行的方法是将所有捕获的变量存储在一个对象上,然后传入该对象。

这是否意味着嵌套函数的每次调用都会在它捕获 scala 中的任何变量时进行分配?

use*_*ser 5

The variable a itself is still in the outer method's stack frame, while the object that it refers to is allocated on the heap, as all Java objects are (even when a is supposed to represent a primitive type).

By running javap -v on that code of yours, we can see that a is actually a final variable of type scala.runtime.IntRef, which holds an integer field that can be updated. The nested method inner is turned into a static method that accepts one argument of type IntRef and sets its elem field to 42. This is somewhat similar to the C# approach, but creates one object for each variable instead of a struct to hold all of them.

public int outer();                                                                                          
descriptor: ()I                                                                                                         
flags: (0x0001) ACC_PUBLIC                                                                                              
Code:                                                                                                                     
stack=1, locals=2, args_size=1                                                                                             
0: iconst_0                                                                                                             
1: invokestatic  #16                 // Method scala/runtime/IntRef.create:
(I)Lscala/runtime/IntRef;                    
4: astore_1                                                                                                             
5: aload_1                                                                                                              
6: invokestatic  #20                 // Method inner$1:(Lscala/runtime/IntRef;)V                                        
9: aload_1                                                                                                             
10: getfield      #24                 // Field scala/runtime/IntRef.elem:I                                              
13: ireturn 
Run Code Online (Sandbox Code Playgroud)

Edit: Let's try it with a String this time:

class ClosureTest {
    def outer = {
        var a = ""
        def inner() = {
            a = "42"
        }
        inner()
        a
    }
}
Run Code Online (Sandbox Code Playgroud)

Output from javap:

public java.lang.String outer();                                                                                          
descriptor: ()Ljava/lang/String;                                                                                        
flags: (0x0001) ACC_PUBLIC                                                                                              
Code:                                                                                                                     
stack=1, locals=2, args_size=1                                                                                             
0: ldc           #12                 // String                                                                          
2: invokestatic  #18                 // Method scala/runtime/ObjectRef.create:(Ljava/lang/Object;)Lscala/runtime/ObjectRef;                                                                                                                     
5: astore_1                                                                                                             
6: aload_1                                                                                                              
7: invokestatic  #22                 // Method inner$1: (Lscala/runtime/ObjectRef;)V                                    
10: aload_1                                                                                                             
11: getfield      #26                 // Field 
scala/runtime/ObjectRef.elem:Ljava/lang/Object;                          
14: checkcast     #28                 // class java/lang/String                                                         
17: areturn
Run Code Online (Sandbox Code Playgroud)

This time, since String is not a primitive, the class ObjectRef (which has a type parameter representing the wrapped value) is used, but it's still basically the same thing. Even though the JVM doesn't allow you to have ref parameters like C# does, objects are still passed by reference, so the value of the object/primitive that a holds can still be modified.

Here's a link to the only documentation I could find. There are lots of other classes, like BooleanRef, FloatRef, and also their volatile counterparts, like VolatileDoubleRef, VolatileObjectRef, etc. Each of these classes basically just have one mutable public field that the compiler uses when the "real" value of the captured variable is needed.