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

Posted

技术标签:

【中文标题】为啥对象表达式中的代码可以从 kotlin 中包含它的范围访问变量?【英文标题】:Why the code in an object expression can access variables from the scope that contains it in kotlin?为什么对象表达式中的代码可以从 kotlin 中包含它的范围访问变量? 【发布时间】:2021-11-12 02:03:44 【问题描述】:

在 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 是怎么做到的?

【问题讨论】:

【参考方案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

包装类的实际实现是用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 书籍,其中包含此信息的基础知识以及此处使用的示例。

【讨论】:

感谢您的回答,对我有帮助 我不明白为什么我们需要通过 Ref 对象包装变量,然后从封闭范围访问它。 我们不需要自己做这件事——Kotlin 编译器会替我们做这件事。我只是在解释它是如何在幕后工作的。【参考方案2】:

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

Higher order functions and lambdas - Closures

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

最后,它是关于编写更少的代码来实现某些目标。如果你想把上面的代码翻译成Java,那就更复杂了。

【讨论】:

要补充一点,它基本上只是使用一个非常简单的包装类,其中包含必须捕获的变量。由于该包装器实例可以是最终的,因此可以在内部类 / lamda 中访问它。 @zsmb13 该信息将证明单独的答案是合理的。捕获闭包通常是这样工作的 - 使用包装器,但由于我不能 100% 确定 Kotlin 究竟是如何做到的,所以我不想在我的答案中包含疯狂的猜测。【参考方案3】:

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

在 Java 中可以应用一个技巧:将可变值包装到 final 包装器中,例如 AtomicReference&lt;T&gt;,编译器将停止抱怨:

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 中更改可变变量。

【讨论】:

能否添加文档链接?【参考方案4】:

如果您使用javap 转储类,您可以找到使用IntRef 而不是int 的kotlin 用于lambda 访问其范围之外的可变变量,因为在java 变量中的 lambda 范围是 effectively-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;

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

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

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

val inc =  immutable + 1 ;

【讨论】:

如果您使用的是 IntelliJ IDEA,Kotlin 插件有一个“反编译为 java”的视图,显示了这个“魔法”。如果您想了解实际发生的情况,它在这种情况下非常有用。【参考方案5】:

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

【讨论】:

以上是关于为啥对象表达式中的代码可以从 kotlin 中包含它的范围访问变量?的主要内容,如果未能解决你的问题,请参考以下文章

为啥标识符可以在 C 中包含“$”? [复制]

Kotlin语法基础,运算符

Kotlin语法基础,运算符

Kotlin语法基础,运算符

在 iOS/XCode 的内置 .frameworks 中包含 Kotlin/Native KDocs 文档

在 C# 中,如何使用反射计算表达式主体输出函数中包含的属性数量?