【水】Kotlin Puzzlers 改

Author Avatar
Dexlind 12月 29, 2017
  • 在其它设备中阅读本文章

《Java Puzzlers》(中文名《Java 解惑》)里面讲解了许多 Java 语言的大坑,相信各位julao应该都看过。Kotlin 作为「a better Java」,在填补一些坑的同时,不可避免地引入了许多新坑。本来本鶸进行了一段时间的取材,想要写一篇《Kotlin Puzzlers》的,可谁知 已经有人早就把我的饭碗抢走了,而且素材比我还多,可恶!

这篇文章就是本鶸看完录像(油土鳖上面有)以及 Github 上的完整内容后,将一些比较坑的谜题拿出来报复社会,蛐蛐一篇观后感而已。

题型为选择题,本辣鸡博客没有NGA的折叠,没有萌百的黑幕,为了防止一眼瞄到答案而造成剧透,本文对答案(以及解释)的摆放位置做了调整,例如第一题的答案被我放在了第二题的位置(以此类推)。各位看客从上到下开始阅读就好了。

使用的 Kotlin 版本为1.2。

没有坑到我的谜题

虽然没有被坑,比较简单,但是值得注意的题。一些更加简单的题目就不放上来了。想刷一遍完整题库的同学可以到 GitHub 上面找。

强力返回 ~ Power Return

fun hello(): String {
  val result = return throw return "Hello"
  println(result.toString())
}

println(hello()) 
这会打印出什么?
a) Hello
b) 两个Hello
c) 这破代码根本没法通过编译
d) 以上答案都不对

本题答案(以及解释)在下一题那里。(以此类推)

计划生育 ~ One Chile Policy

open class Node(val name: String) {
  fun lookup() = println(name)
}

class Parent : Node("parent") {
  fun child(name: String): Node? = Node(name)

  val child1 = child("child1")?.apply { lookup() }
  val child2 = child("child2").apply { lookup() }
}

Parent()
这会打印出什么?
a) child1 和 child2
b) child1 和 parent
c) parent 和 child2
d) 以上答案都不对

上一题的答案:a

要记住,return ***throw *** 都是表达式,其结果的类型为 NothingNothing 类型是任意类型的子类型,所以 Nothing 可以被抛出,可以被返回,可以赋值给任意类型的变量。事实上 hello()return "Hello" 的时候已经结束了,剩下的 throwval resultprintln() 什么的都是不可到达代码(unreachable code),不会被运行。

PS:你甚至可以写出这样的代码:throw throw throw Exception()

两只拉姆达跑得快 ~ Two Lambda

typealias L = (String) -> Unit

fun foo(one: L = {}, two: L = {}) {
  one("one")
  two("two")
}

foo { print(it) }
foo({ print(it) })
这会打印出什么?
a) oneone
b) twotwo
c) onetwo
d) 以上答案都不对

上一题的答案:d

事实上是 child1parentKotlin 的这些扩展方法如 applyletalso 等等都是适用于所有类型的,包括可空类型。child2 那行 apply 函数接收的拉姆达表达式的类型其实是 Node?.() -> Unit,如果 child2那行代码是写在 Parent 类的外面的话,你就会发现这行代码根本没法通过编译,这里面调用的 lookup 实际上是 parentlookup。(你可以把 apply 换成 also 试试。)

衔尾蛇 ~ Cyclic Object Constructions

open class A(val x: Any?)

object B : A(C)
object C : A(B)

print(B.x)
print(C.x)
这会打印出什么?
a) nullnull
b) C@********null
c) ExceptionInInitializerError
d) 这破代码根本没法通过编译

上一题的答案:d

实际上是 twoone。第一句的语法只有在拉姆达表达式是最后一个参数的时候才能写的,所以是 two。第二句是普通的方法调用,先填上第一个参数,第二个参数使用默认值。

PS:想要朴素地实现 foo { } { } 这样的调用的话应该是办不到的吧。(如果能做到请赶快告诉我!)

哇,好长 ~ Breaking Lines

val anExtremelyLongAndBoringStatementThatBarelyFitsOnALine = 2
val anotherExtremelyLongStatementThatBarelyFitsOnALine = 2
val someList = listOf(1)
val result = someList.map {
        anExtremelyLongAndBoringStatementThatBarelyFitsOnALine
            + anotherExtremelyLongStatementThatBarelyFitsOnALine
}
print(result)
这会打印出什么?
a) [1]
b) [2]
c) [4]
d) [1, 4]

上一题的答案:b

B 初始化需要 CC 初始化需要 B。咦,B 还没初始化完成呢,那么哪来的 B 呢,只能是 null 了啊!

参见 http://jetbrains.github.io/kotlin-spec/#_singleton_objects

换个名字你就不认识我了 ~ Good Child Has Many Names

open class C {
  open fun sum(x: Int = 1, y: Int = 2): Int = x + y
}

class D : C() {
  override fun sum(y: Int, x: Int): Int = super.sum(x, y)
}

val d: D = D()
val c: C = d
print(c.sum(x = 0))
print(d.sum(x = 0))
println()
这会打印出什么?
a) 22
b) 11
c) 21
d) 这破代码根本没法通过编译

上一题的答案:b

之前裙里有julao问过类似的问题所以我没被坑到。你可以把代码丢到IDEA里面,光标定位到加号前面,按下 Ctrl+B 或者 Ctrl+Q,看看那个加号是什么意思吧。解决方法:把加号放在上一行的后面可破。

排序 ~ Sorting

val list = arrayListOf(1, 5, 3, 2, 4)

val sortedList = list.sort()

print(sortedList)
这会打印出什么?
a) [1, 5, 3, 2, 4]
b) [1, 2, 3, 4, 5]
c) kotlin.Unit
d) 这破代码根本没法通过编译

上一题的答案:c

命名参数是静态分配的。

致命的顺序 ~ The Order

class Order {
  private val c: String
  init {
    the()
    c = ""
  }
  private fun the() {
    println(c.length)
  }
}

Order()
这会打印出什么?
a) 0
b) null
c) 这破代码根本没法通过编译
d) 以上答案都不对

上一题的答案:c

参见 https://zhuanlan.zhihu.com/p/27234651

本题的答案:d

JVM 不想理你并向你抛出了一只 NPE。Java 也有这个问题,Scala 不熟悉不清楚。据说 Ceylon 就没有这个问题,具体可以看 Ceylon 官网上的说明(趁机吹一波 Ceylon)。

https://ceylon-lang.org/documentation/1.3/tour/initialization/#definite_assignment_and_definite_initialization

Kotlin 官方人员曾表示修复这个缺陷是一件十分困难的事。

那些坑了我的迷题

我果然是鶸,错了这么多,进入自卑模式~

区间测试 ~ Inclusive Range

val i = 10.5

when (i) {
  in 1..10 -> println("in")
  !in 1..10 -> println("!in")
  else -> println("else")
}
这会打印出什么?
a) in
b) !in
c) else
d) 这破代码根本没法通过编译

据说这道题在 Kotlin 1.0 版本和 1.2 版本里有不同的表现。(我懒得试旧版本了)

极性不定 ~ Weird Chaining

fun printNumberSign(num: Int) {
  if (num < 0) {
    "negative"
  } else if (num > 0) {
    "positive"
  } else {
    "zero"
  }.let { println(it) }
}

printNumberSign(-2)
printNumberSign(0)
printNumberSign(2)
这会打印出什么?
a) negative; zero; positive
b) negative; zero
c) negative; positive
d) zero; positive

上一题的答案:a

实际上是把 i 转成 Int 再进行的比较。

$_$ ~ Dollar In Multiline Literals

val multiline = """
        To win \$999.999 execute "rm -fr \$HOME/kotlin-puzzlers/*"
        """.trimIndent()

println(multiline)
这会打印出什么?
a) To win \$999.999 execute "rm -fr \$HOME/kotlin-puzzlers/*"
b) To win 999.999 execute "rm -fr \/home/user/kotlin-puzzlers/*"
c) To win $999.999 execute "rm -fr $HOME/kotlin-puzzlers/*"
d) 这破代码根本没法通过编译

上一题的答案:d

相当于:

if (num < 0) {
  "negative"
} else {
  if (num > 0) { "positive" }
  else { "zero" }.let { println(it) }
}

秒懂!

解决方法:用小括号将那串 if else 括起来再接 let 可破。

你的名字是 ~ Property Override

open class Named {
  open var name: String? = null
    get() = field ?: "<unnamed>"
}

class Person: Named() {
  override var name: String? = null
    get() = super.name
    set(value) { field = "Mr $value" }
}

val person = Person()
person.name = "Anton"
println(person.name)
这会打印出什么?
a) Anton
b) Mr Anton
c) <unnamed>
d) null

上一题的答案:d

这种 raw string 里面美元符号 $ 一直都是表示模板表达式,而且不能被转义,所以 $HOME 这里糟了。(你问为什么 $999.999 没糟?因为 999.999 不是合法的变量名啊,你在 999.999 两边加上反引号试试。)

解决方法:”””${‘$’}HOME”””

冰雪聪明 ~ Custom Getter Smartcast

class SmartCastable {
    val list: List<Int> = mutableListOf(1, 2, 3)

    val set: Set<Int> = mutableSetOf(1, 2, 3)
         get() = field
}

val sc = SmartCastable()

if(sc.list is MutableList)
    sc.list.add(4)

if(sc.set is MutableSet)
    sc.set.add(4)

println("${sc.list}, ${sc.set}")
这会打印出什么?
a) [1, 2, 3], [1, 2, 3]
b) [1, 2, 3, 4], [1, 2, 3, 4]
c) UnsupportedOperationException
d) 这破代码根本没法通过编译

上一题的答案:c

这里有两个 backing fieldNamed 类的那个 get 方法操纵了父类的 backing fieldset 方法操纵的是自己的 backing field

解决方法:

class Person: Named() {
    override var name: String?
        get() = super.name
        set(value) { super.name = "Mr $value" }
}

最小值 ~ MinInt

fun printInt(n: Int) {
  println(n)
}

printInt(-2_147_483_648.inc())
这会打印出什么?
a) -2147483647
b) -2147483649
c)  2147483647
d) 以上答案都不对

上一题的答案:d

sc.set 有一个自定义 getter,编译器没法判断这个 getter 返回的是否是同一个对象,所以无法进行智能转换(smart cast)。

解决方法:这时候别声明只有 getter 的属性,声明有 backing field 的属性就好。或者像这样:

val set = sc.set
if(set is MutableSet)
    set.add(4)

人类衰退之后 ~ Population To Mars

class Population(var cities: Map<String, Int>) {
  val 帝都 by cities
  val 魔都 by cities
  val 妖都 by cities
}

val population = Population(mapOf(
    "帝都" to 864_816,
    "魔都" to 413_782,
    "妖都" to 43_005
))

// 许多年过去了,地球毁灭了,只有少数幸存者抵达了火星(大吉大利今晚吃鸡)!
population.cities = emptyMap()

with(population) {
  println("$帝都; $魔都; $妖都")
}
这会打印出什么?
a) 0; 0; 0
b) 864816; 413782; 43005
c) NullPointerException
d) NoSuchElementException

上一题的答案:d

破代码没法通过编译。实际的求值顺序是:-(2_147_483_648.inc()),这TM是个 Long。这个一元操作符的优先级比普通方法调用低。

反物质 ~ AntiMatter

operator fun Nothing?.not() = Unit
operator fun Unit.not() = null

val foo = null

println(!!!!!foo!!!!)
这会打印出什么?
a) null
b) kotlin.Unit
c) KotlinNullPointerException
d) 这破代码根本没法通过编译

上一题的答案:b

用于委托代理的那个 Map 被保存在了一个 private finalfield 里面,正常手段没法赋新值。

本题的答案:d

null 的类型是 Nothing?(而且是这个类型的唯一值)。***!! 这个非空断言比 not() 的优先级要高,所以 foo!!!! 的类型是 NothingNothing 是所有类型的子类型,所以编译器没法判断该调用哪个扩展方法。

总结

这些辣鸡代码,别学。

知识共享许可协议
本作品采用知识共享 署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可,转载请注明出处。

本文链接:https://aisia.moe/2017/12/28/kotlin-puzzlers/