18

我目前正在研究不同语言的闭包实现。然而,当谈到 Scala 时,我找不到任何关于如何将闭包映射到 Java 对象的文档。

Scala 函数映射到 FunctionN 对象是有据可查的。我假设对闭包的自由变量的引用必须存储在该函数对象的某个位置(就像在 C++0x 中所做的那样)。

我还尝试使用 scalac 编译以下内容,然后使用 JD 反编译类文件:

object ClosureExample extends Application { 
  def addN(n: Int) = (a: Int) => a + n
  var add5 = addN(5)
  println(add5(20))
}

在反编译的源代码中,我看到了 Function1 的匿名子类型,它应该是我的闭包。但是 apply() 方法是空的,并且匿名类没有字段(可能存储闭包变量)。我想反编译器没有设法从类文件中得到有趣的部分......

现在的问题:

  • 你知道转换是如何完成的吗?
  • 你知道它记录在哪里吗?
  • 你有其他想法我可以如何解开这个谜吗?
4

2 回答 2

31

让我们分解一组示例,以便我们了解它们的不同之处。(如果使用 RC1,请编译-no-specialization以使事情更容易理解。)

class Close {
  var n = 5
  def method(i: Int) = i+n
  def function = (i: Int) => i+5
  def closure = (i: Int) => i+n
  def mixed(m: Int) = (i: Int) => i+m
}

首先,让我们看看有什么method作用:

public int method(int);
  Code:
   0:   iload_1
   1:   aload_0
   2:   invokevirtual   #17; //Method n:()I
   5:   iadd
   6:   ireturn

很简单。这是一种方法。加载参数,调用 getter n,添加,返回。看起来就像Java。

怎么样function?它实际上并没有关闭任何数据,但它一个匿名函数(称为Close$$anonfun$function$1)。如果我们忽略任何特化,构造函数和应用是最感兴趣的:

public scala.Function1 function();
  Code:
   0:   new #34; //class Close$$anonfun$function$1
   3:   dup
   4:   aload_0
   5:   invokespecial   #35; //Method Close$$anonfun$function$1."<init>":(LClose;)V
   8:   areturn

public Close$$anonfun$function$1(Close);
  Code:
   0:   aload_0
   1:   invokespecial   #43; //Method scala/runtime/AbstractFunction1."<init>":()V
   4:   return

public final java.lang.Object apply(java.lang.Object);
  Code:
   0:   aload_0
   1:   aload_1
   2:   invokestatic    #26; //Method scala/runtime/BoxesRunTime.unboxToInt:(Ljava/lang/Object;)I
   5:   invokevirtual   #28; //Method apply:(I)I
   8:   invokestatic    #32; //Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
   11:  areturn

public final int apply(int);
  Code:
   0:   iload_1
   1:   iconst_5
   2:   iadd
   3:   ireturn

因此,您加载一个“this”指针并创建一个以封闭类作为其参数的新对象。这是任何内部阶级的标准,真的。该函数不需要对外部类做任何事情,所以它只调用超级的构造函数。然后,在调用 apply 时,您执行 box/unbox 技巧,然后调用实际的数学运算——也就是说,只需添加 5。

但是如果我们在 Close 中使用变量的闭包呢?设置完全一样,但现在构造函数Close$$anonfun$closure$1看起来像这样:

public Close$$anonfun$closure$1(Close);
  Code:
   0:   aload_1
   1:   ifnonnull   12
   4:   new #48; //class java/lang/NullPointerException
   7:   dup
   8:   invokespecial   #50; //Method java/lang/NullPointerException."<init>":()V
   11:  athrow
   12:  aload_0
   13:  aload_1
   14:  putfield    #18; //Field $outer:LClose;
   17:  aload_0
   18:  invokespecial   #53; //Method scala/runtime/AbstractFunction1."<init>":()V
   21:  return

也就是说,它检查以确保输入非空(即外部类非空)并将其保存在字段中。现在到了应用它的时候,在装箱/拆箱包装器之后:

public final int apply(int);
  Code:
   0:   iload_1
   1:   aload_0
   2:   getfield    #18; //Field $outer:LClose;
   5:   invokevirtual   #24; //Method Close.n:()I
   8:   iadd
   9:   ireturn

您会看到它使用该字段来引用父类,并为n. 添加,返回,完成。因此,闭包很简单:匿名函数构造函数只是将封闭类保存在私有字段中。

现在,如果我们关闭的不是内部变量,而是方法参数呢?就是Close$$anonfun$mixed$1这样。首先,看看这个mixed方法做了什么:

public scala.Function1 mixed(int);
  Code:
   0:   new #39; //class Close$$anonfun$mixed$1
   3:   dup
   4:   aload_0
   5:   iload_1
   6:   invokespecial   #42; //Method Close$$anonfun$mixed$1."<init>":(LClose;I)V
   9:   areturn

m它在调用构造函数之前加载参数!所以构造函数看起来是这样的也就不足为奇了:

public Close$$anonfun$mixed$1(Close, int);
  Code:
   0:   aload_0
   1:   iload_2
   2:   putfield    #18; //Field m$1:I
   5:   aload_0
   6:   invokespecial   #43; //Method scala/runtime/AbstractFunction1."<init>":()V
   9:   return

该参数保存在私有字段中。没有保留对外部类的引用,因为我们不需要它。你也不应该对 apply 感到惊讶:

public final int apply(int);
  Code:
   0:   iload_1
   1:   aload_0
   2:   getfield    #18; //Field m$1:I
   5:   iadd
   6:   ireturn

是的,我们只需加载该存储字段并进行数学运算。

我不确定您在做什么以在您的示例中看不到这一点——对象有点棘手,因为它们同时具有MyObjectMyObject$类,并且方法以一种可能不直观的方式在两者之间拆分。但是 apply 肯定会应用事物,总体而言,整个系统几乎按照您期望的方式运行(在您坐下来认真思考很长时间之后)。

于 2010-04-19T20:53:15.327 回答
5

与 Java 的匿名内部类是伪闭包并且无法修改看似封闭到其环境中的变量不同,Scala 的闭包是真实的,因此闭包代码直接引用周围环境中的值。当从闭包中引用这些值时,它们的编译方式会有所不同,以便实现这一点(因为方法代码无法从除当前帧之外的任何激活帧访问本地变量)。

相反,在 Java 中,它们的值被复制到内部类的字段中,这就是语言要求封闭环境中的原始值是 的原因final,因此它们永远不会发散。

因为所有 Scala 函数字面量/闭包对封闭环境中的值的引用都在函数字面量apply()方法的代码中,所以它们不会作为Function为函数字面量生成的实际子类中的字段出现。

我不知道你是如何反编译的,但你如何反编译的细节可能解释了为什么你没有看到apply()方法体的任何代码。

于 2010-04-19T20:28:28.737 回答