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

为什么对象表达式中的代码可以从kotlin中包含它的范围访问变量?,kotlin,Kotlin,在Kotlin中,对象表达式中的代码可以访问包含它的范围中的变量,就像下面的代码一样: fun countClicks(window: JComponent) { var clickCount = 0 var enterCount = 0 window.addMouseListener(object : MouseAdapter() { override fun mouseClicked(e: MouseEvent) { clickCount++

在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中,不允许这样做,因为对象的生命周期与局部变量不同,因此当您尝试访问对象时,
enterCount
clickCount
可能无效。有人能告诉我Kotlin是如何做到这一点的吗?

在Kotlin中,与Java不同,lambda表达式或匿名函数(以及本地函数和对象表达式)可以访问和修改它们的闭包——在外部范围中声明的变量。此行为符合设计要求

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


最后是写更少的代码来实现一些东西。如果您想将上述代码翻译成Java,它将更加复杂

如果您使用
javap
转储类,您可以使用
IntRef
而不是
int
来查找kotlin,以便lambda访问其范围之外的可变变量,由于lambda作用域之外的java内变量是有效的finalfinal,这意味着您根本无法修改变量,例如:

// 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;
var mutable = 1;
val immutable = mutable; // kotlin compiler will using int 

val inc = { immutable + 1 };
上面的代码是相等的。因此,不要在多线程中修改lambda范围之外的可变变量。这将导致错误的结果

另一个很好的用法是,您不需要修改lambda范围之外的可变变量,而需要改进性能优化。您可以使用附加的不可变的变量来实现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;
var mutable = 1;
val immutable = mutable; // kotlin compiler will using int 

val inc = { immutable + 1 };

在Java中,您只能(有效地)捕获匿名类和lambda中的最终变量。在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
class-Ref(var-value:T)//运行时某处的通用包装类
val计数器=参考(0);//包装值为0的Int
{counter.value++}()//捕获计数器并递增其存储值
包装器类的实际实现是用Java编写的,如下所示:

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

    @Override
    public String toString() {
        return String.valueOf(element);
    }
}
公共静态最终类ObjectRef实现可序列化{
公共T元素;
@凌驾
公共字符串toString(){
返回字符串.valueOf(元素);
}
}
还有一些附加的包装器,称为
ByteRef
ShortRef
,等等,它们包装各种原语,以便不必为了捕获而装箱。您可以在中找到所有包装器类


这本书包含了这些信息的基础知识和这里使用的示例。

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

在Java中可以应用一个技巧:将可变值包装到
final
包装器中,如
原子引用
,编译器将停止抱怨:

AtomicReference<Integer> max = new AtomicReference<>(10);
if (System.currentTimeMillis() % 2 == 0) {
    max.set(11)
}
Predicate<Integer> pred = i -> i * 2 > max.get();
Ref
变量是
final
,因此可以毫无问题地捕获。因此,可变变量可以从lambda中更改。


在Android Studio 3.2中,这个漂亮的小消息告诉您闭包中的
projectType
var发生了什么。

为了补充这一点,它基本上只使用一个非常简单的包装类,其中包含必须捕获的变量。由于该包装器实例可以是最终的,因此可以在内部类/lamda中访问它。@zsmb13该信息将证明单独的答案是正确的。捕获闭包通常是这样工作的-使用包装器,但由于我不能100%确定Kotlin到底是如何做到的,所以我不想在我的答案中包含疯狂的猜测。谢谢你的回答,这有助于我理解为什么我们需要通过Ref对象包装变量,然后从封闭范围访问它。我们不需要自己做这件事-Kotlin编译器为我们做这件事。我只是在解释它在幕后是如何工作的。你能添加一个到文档的链接吗?如果你使用IntelliJ IDEA,Kotlin插件有一个“反编译到java”视图,显示了这种“魔力”。如果你想了解到底发生了什么,在这样的情况下它是非常有用的。