为啥对象表达式中的代码可以从 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中是不允许这样做的,因为对象的生命周期与局部变量不同,所以enterCount
或clickCount
可能在你尝试访问对象时无效。谁能告诉我 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);
还有称为ByteRef
、ShortRef
等的附加包装器,它们包装各种原语,这样它们就不必被装箱就可以被捕获。您可以在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<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 中更改可变变量。
【讨论】:
能否添加文档链接?【参考方案4】:如果您使用javap
转储类,您可以找到使用IntRef
而不是int
的kotlin 用于lambda 访问其范围之外的可变变量,因为在java 变量中的 lambda 范围是 effectively-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 范围之外的 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 中包含它的范围访问变量?的主要内容,如果未能解决你的问题,请参考以下文章