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

问题描述 投票:18回答:5

在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++
    }
   })
}

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

kotlin
5个回答
19
投票

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

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

所以当你这样做时:

var counter = 0
{ counter++ }()   // definition and immediate invocation, very JavaScript

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

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

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

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

    @Override
    public String toString() {
        return String.valueOf(element);
    }
}

还有一些名为ByteRefShortRef等的包装器,它们包装各种基元,这样它们就不必被盒装以便被捕获。您可以在this file中找到所有包装类。

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


5
投票

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

Higher order functions and lambdas - Closures

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

最后,它是关于编写更少的代码来实现某些目标。如果要将上述代码转换为Java,则会更复杂。


3
投票

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

有一个可以在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();

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

class Ref<T>(var value: T)

Ref变量是final,因此可以毫无问题地捕获。结果,可变变量可以在lambda内改变。


2
投票

如果你通过使用javap转储类,你可以使用IntRef而不是int找到kotlin,因为lambda访问其范围内的可变变量,因为在lambda范围之外的java变量是有效的final或final,这意味着你不能完全修改变量,例如:

// 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;

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

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

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

val inc = { immutable + 1 };

0
投票

enter image description here使用Android Studio 3.2,这个漂亮的小消息告诉你在封闭内部发生了什么projectType var。

热门问题
推荐问题
最新问题