使用 DefaultValue 委托映射的 Kotlin 问题 - 语言错误?
Kotlin Problem Delegating to Map with DefaultValue - Language Bug?
在下面的代码中,MyMap
简单地实现了 Map
by
对 impl
的委托:
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 映射。
在下面的代码中,MyMap
简单地实现了 Map
by
对 impl
的委托:
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 映射。