为什么对象表达式中的代码可以从kotlin中包含它的范围访问变量?

Lin*_*Guo 19 kotlin

在Kotlin中,对象表达式中的代码可以从包含它的作用域访问变量,就像下面的代码一样:

fun countClicks(window: JComponent) {
   var clickCount = 0
   var enterCount = 0
   window.addMouseListener(object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) {
        clickCount++
    }

    override fun mouseEntered(e: MouseEvent) {
        enterCount++
    }
   })
}
Run Code Online (Sandbox Code Playgroud)

但为什么?在Java中,不允许这样做,因为对象的生命周期与局部变量不同,因此当您尝试访问对象时,enterCount或者clickCount可能无效.有人能告诉我Kotlin是怎么做到的吗?

zsm*_*b13 23

在Java中,您只能捕获(有效)匿名类和lambdas中的最终变量.在Kotlin中,您可以捕获任何变量,即使它们是可变的.

这是通过将任何捕获的变量包装在简单包装类的实例中来完成的.这些包装器只有一个包含捕获变量的字段.由于包装器的实例可以final,它们可以照常捕获.

所以当你这样做时:

var counter = 0
{ counter++ }()   // definition and immediate invocation, very JavaScript
Run Code Online (Sandbox Code Playgroud)

这样的事情发生在引擎盖下:

class Ref<T>(var value: T) // generic wrapper class somewhere in the runtime

val counter = Ref(0);      // wraps an Int of value 0
{ counter.value++ }()      // captures counter and increments its stored value
Run Code Online (Sandbox Code Playgroud)

包装类的实际实现是用Java编写的,如下所示:

public static final class ObjectRef<T> implements Serializable {
    public T element;

    @Override
    public String toString() {
        return String.valueOf(element);
    }
}
Run Code Online (Sandbox Code Playgroud)

还有一些附加的包装叫ByteRef,ShortRef等那个包裹的各种原语,使他们不必为了装箱被捕获做.您可以在此文件中找到所有包装类.

学分转至Kotlin in Action一书,其中包含此信息的基础知识以及此处使用的示例.

  • 我们不需要自己执行此操作-Kotlin编译器会为我们执行此操作。我只是在解释它在后台的工作方式。 (2认同)

Dal*_*kar 5

在Kotlin中,与Java不同,lambda表达式或匿名函数(以​​及本地函数和对象表达式)可以访问和修改其闭包-在外部范围内声明的变量。此行为是设计使然。

高阶函数和lambda-闭包

为什么Java不允许这样做而Kotlin允许-捕获闭包会引入额外的运行时开销。Java使用简单快速的方法以功能为代价。另一方面,Kotlin为您提供了更多功能-功能,但它也会在后台生成更多代码来支持它。

最后,它是关于减少编写代码以实现目标的方法。如果您想将上述代码转换为Java,它将更加复杂。


hol*_*ava 5

如果你通过使用转储类,javap你会发现 kotlin 使用IntRefnot a intfor lambda 访问超出其范围的可变变量,因为在 java 中,超出 lambda 范围的变量实际上是最终的最终的,这意味着你不能修改该变量根本没有,例如:

// Kotlin 
var value = 1; // kotlin compiler will using IntRef for mutable variable
val inc = { value++ }; 
inc();
println(value);// 2;

//Java
IntRef value = new IntRef();
value.element = 1;

Runnable inc=()-> value.element++;
inc.run();
println(value.element);// 2;
Run Code Online (Sandbox Code Playgroud)

上面的代码是平等的。所以不要在多线程中修改 lambda 范围之外的可变变量。这会导致错误的结果。

另一个好的用法是您不需要修改lambda 范围之外的可变变量并希望提高性能优化。您可以使用额外的不可变变量来实现 lambda 的性能,例如:

var mutable = 1;
val immutable = mutable; // kotlin compiler will using int 

val inc = { immutable + 1 };
Run Code Online (Sandbox Code Playgroud)


s1m*_*nw1 5

您所指的概念称为“捕获”

有一个可以在 Java 中应用的技巧:将可变值包装到包装器中final,例如AtomicReference<T>,编译器将停止抱怨:

AtomicReference<Integer> max = new AtomicReference<>(10);
if (System.currentTimeMillis() % 2 == 0) {
    max.set(11)
}
Predicate<Integer> pred = i -> i * 2 > max.get();
Run Code Online (Sandbox Code Playgroud)

这基本上也是 Kotlin 中发生的情况:如果val捕获了 Final 变量 (),它只会被复制到 lambda 中。但另一方面,如果捕获了可变变量 ( var)Ref ,则其值将包装在 的实例中:

class Ref<T>(var value: T)
Run Code Online (Sandbox Code Playgroud)

Ref变量是final,因此可以毫无问题地捕获。因此,可以从 lambda 内部更改可变变量。