使用 DefaultValue 委托映射的 Kotlin 问题 - 语言错误?

Kotlin Problem Delegating to Map with DefaultValue - Language Bug?

在下面的代码中,MyMap 简单地实现了 Map byimpl 的委托:

foo@host:/tmp$ cat Foo.kt
class MyMap <K, V> (val impl : Map <K, V>) : Map<K, V> by impl {
  fun myGetValue (k: K) = impl.getValue(k)
}

fun main() {
  val my_map = MyMap(mapOf('a' to 1, 'b' to 2).withDefault { 42 })
  println(my_map.myGetValue('c'))  // OK
  println(my_map.getValue('c'))    // ERROR
}

为什么我在第二个 println 上出现以下错误?

foo@host:/tmp$ /path/to/kotlinc Foo.kt
foo@host:/tmp$ /path/to/kotlin FooKt
42
Exception in thread "main" java.util.NoSuchElementException: Key c is missing in the map.
        at kotlin.collections.MapsKt__MapWithDefaultKt.getOrImplicitDefaultNullable(MapWithDefault.kt:24)
        at kotlin.collections.MapsKt__MapsKt.getValue(Maps.kt:344)
        at FooKt.main(Foo.kt:8)
        at FooKt.main(Foo.kt)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:568)
        at org.jetbrains.kotlin.runner.AbstractRunner.run(runners.kt:64)
        at org.jetbrains.kotlin.runner.Main.run(Main.kt:176)
        at org.jetbrains.kotlin.runner.Main.main(Main.kt:186)
foo@bigdev:/tmp$

更新:编译器和运行时版本输出为:

foo@host:/tmp$ kotlinc -version
info: kotlinc-jvm 1.6.10 (JRE 17.0.1+12-LTS)
foo@host:/tmp$ kotlin -version
Kotlin version 1.6.10-release-923 (JRE 17.0.1+12-LTS)
foo@host:/tmp$ javac -version
javac 17.0.1
foo@host:/tmp$ java -version
openjdk version "17.0.1" 2021-10-19 LTS
OpenJDK Runtime Environment Corretto-17.0.1.12.1 (build 17.0.1+12-LTS)
OpenJDK 64-Bit Server VM Corretto-17.0.1.12.1 (build 17.0.1+12-LTS, mixed mode, sharing)

老实说,虽然我希望您的代码能够正常工作,但这可能是错误,但我们必须查看生成的字节码。

documentation 它说(强调我的):

This implicit default value is used when the original map doesn't contain a value for the key specified and a value is obtained with Map.getValue function, for example when properties are delegated to the map.

“合同”的冲突来自实际的 Map 界面,它说:

Returns the value corresponding to the given [key], or null if such a key is not present in the map.

地图默认合同必须满足此要求,因此当键为 non-existent.

时,它可以“仅”return null

我发现了一个关于此的讨论 in the Kotlin forums

发生这种情况是因为 withDefault 的实现方式有点出乎意料。 withDefault 生成的包装器不会覆盖 getValue() 因为这是不可能的,因为 getValue() 是一个扩展函数。所以不幸的是,我们拥有的是 classic OOP anti-pattern:getValue() 执行 is 检查是否在内部 MapWithDefault 接口上调用它,并且仅在这种情况下使用默认值。我看不出他们有什么办法可以在不违反 Map 合同的情况下避免这种情况。

myGetValue 在底层委托上调用 getValue,这是一个 MapWithDefault,所以它工作正常。

在您的 MyMap 实例上调用的

getValue 将无法通过内部 is MapWithDefault 检查,因为 MyMap 不是 MapWithDefault,即使它的代表是。其他类型的委托不会传播到委托给它的 class,这是有道理的。就像我们委托给一个 MutableMap 一样,我们可能希望 class 被认为只是一个 read-only 映射。