Kotlin鱿鱼游戏大奖赛
点击上方蓝字关注我,知识会给你力量
鱿鱼游戏来了,现在开始,看看你闯过第几关。
在不借助IDE的情况下,看你的人肉编译器能否编译出正确的结果。
Scala-like functions
fun hello() = {
println("Hello, World")
}
hello()
-
a). Does not compile -
b). Prints “Hello, World” -
c). Nothing -
d). Something else
提示:在IDE里面,lint会提示,Unused return value of a function with lambda expression body
答案:C
要执行这个lambda,需要使用hello()(),或者使用hello().invoke()
这让我想到了Flutter中的一个骚操作:immediately invoked function expression (IIFE),即(){}()
Indent trimming
val world = "multiline world"
println(
"""
Hello
\$world
""".trimIndent()
)
-
a)
Hello
$world
-
b)
Hello
$world
-
c)
Hello
\multiline world
-
d) Doesn’t compile
答案:C
在Kotlin中,raw tring是由一个三引号("")定义的,不包含转义,但可以包含换行符和任何其他字符。即使有转义字符\, string,那么我们需要使用${'$'}来代替。
If-else chaining
fun printNumberSign(num: Int) {
if (num < 0) {
"negative"
} else if (num > 0) {
"positive"
} else {
"zero"
}.let { Log.d("xys", it) }
}
printNumberSign(-2)
Log.d("xys", ",")
printNumberSign(0)
Log.d("xys", ",")
printNumberSign(2)
-
a) negative,zero,positive -
b) negative,zero, -
c) negative,,positive -
d) ,zero,positive
答案:D
,
zero
,
positive
请记住,在Java编译器处理之后,else if结构实际上只是用一个"单行"if调用else来处理的,也就是说,不管有多少个else if,实际是都会转化为else中的嵌套。在Kotlin中,函数在if-else块之前被解析,所以.let { print(it) }只适用于最后的else if。所以在这种情况下,第一个if语句的结果将不会被使用,函数将立即返回。为了避免这种情况,你可以将整个if ... else ... 包裹在小括号中,然后在其上加上.let。
Lambda runnables
fun run() {
val run: () -> Unit = {
println("Run run run!")
}
Runnable { run() }.run()
}
调用:
run()
-
a) “Run run run!” -
b) Doesn’t compile -
c) StackOverflowError -
d) None of the above
答案:A
这道题实际上是考察的Kotlin局部函数的使用,上面的代码,实际上等价于下面的代码:
val run1: () -> Unit = {
println("Run run run!")
}
fun run() {
Runnable { run1() }.run()
}
使用局部函数,可以将逻辑隐藏在函数内部。
Making open abstract
open class A {
open fun a() {}
}
abstract class B: A() {
abstract override fun a()
}
open class C: B()
-
a) Compiles fine -
b) Error: Class ‘C’ is not abstract and does not implement abstract base class member -
c) Error: ‘a’ overrides nothing -
d) Error: Function ‘a’ must have a body
答案:B
我们可以用抽象的函数来覆写一个open的函数,但这样它还是抽象的,我们需要在所有子类中覆写需要实现的方法,像下面这样的代码就可以执行了。
open class A {
open fun a() {}
}
abstract class B: A() {
abstract override fun a()
}
open class C: B() {
override fun a() {}
}
C().a()
List minus list
val list = listOf(1, 2, 3)
println(list - 1)
println(list - listOf(1))
val ones = listOf(1, 1, 1)
println(ones - 1)
println(ones - listOf(1))
选项:
a) [2, 3][2, 3][1, 1][1, 1]
b) [2, 3][2, 3][1, 1][]
c) [1, 3][2, 3][][1, 1]
d) [2, 3][2, 3][][]
答案:B
这道题实际上就是考察minus函数的实现,在Kotlin中:
-
List minus T :移除第一个匹配的元素 -
List minus List :从第一个List中,移除第二个List中存在的所有元素
Composition
operator fun (() -> Unit).plus(f: () -> Unit): () -> Unit = {
this()
f()
}
({ print("Hello, ") } + { print("World") })()
-
a) “Hello, World” -
b) Error: Expecting top-level declaration -
c) Error: Expression f cannot be invoked as a function -
d) Error: Unresolved reference (operator + not defined for this types) -
e) Works, but prints nothing
答案:A
操作符重载函数plus的定义是完全正确的。它返回新的函数(使用lambda表达式创建),该函数由两个作为参数的函数组成。当我们添加两个函数时,我们就有了另一个可以调用的函数。当我们调用它时,我们有一个接一个的lambda表达式被调用。
What am I?
val whatAmI = {}()
println(whatAmI)
-
a) “null” -
b) “kotlin.Unit” -
c) Doesn’t print anything -
d) Doesn’t compile
答案:B
这道题考察的是lambda表达式的基本知识,这个lambda表达式没有返回内容,所以它的类型就是Unit。
Return return
fun f1(): Int {
return return 42
}
fun f2() {
throw throw Exception()
}
f1和f2能执行吗?
-
a) returns 42; throws exception -
b) returns 42; doesn’t compile -
c) doesn’t compile; throws exception -
d) doesn’t compile; doesn’t compile
答案:A
f1中的第一个return,其实无效,如果在IDE中,就会有Lint提示。
return表达式有返回类型,可以作为表达式使用,在f1中,它也以结果42结束f1的执行。同样地,throw声明类型——Nothing也是一个返回类型,所以两个函数都能编译,但是在f2调用的时候,会以异常结束。
Extensions are resolved statically
open class C
class D : C()
fun C.foo() = "c"
fun D.foo() = "d"
fun printFoo(c: C) {
println(c.foo())
}
调用:
printFoo(D())
-
a) Doesn’t compile -
b) Runtime error -
c) c -
d) d
答案:C
这个例子考察的是拓展函数的具体实现原理,因为被调用的扩展函数只依赖于参数c的声明类型,也就是C类,所以,它只会调用C类的拓展函数foo。
扩展实际上并不会修改它们所扩展的类。通过定义一个扩展函数,你并没有真实的在一个类中插入新的成员,而只是让新的函数可以在这个类型的变量上用点号来调用,相当于一层Wrapper。
Expression or not
fun f1() {
var i = 0
val j = i = 42
println(j)
}
fun f2() {
val f = fun() = 42
println(f)
}
fun f3() {
val c = class C
println(c)
}
f1、f2、f3的结果分别是什么?
-
a)
42 () -> kotlin.Int class C
-
b)
42 () -> kotlin.Int doesn’t compile
-
c)
doesn’t compile () -> kotlin.Int doesn’t compile
-
d)
doesn’t compile doesn’t compile doesn’t compile
答案:C
变量初始化和类声明都是Kotlin中的语句,它们没有声明任何返回类型,所以我们不能将这种声明分配给变量,因此不能编译。而在f2中我们实际上是实现了一个匿名函数,所以输出一个函数。
Eager or lazy?
val x = listOf(1, 2, 3).filter {
print("$it ")
it >= 2
}
print("before sum ")
println(x.sum())
-
a) 1 2 3 before sum 5 -
b) 2 3 before sum 5 -
c) before sum 1 2 3 5 -
d) order is not deterministic
答案:A
与Java8的Stream API不同,Kotlin中的集合扩展函数是Eager的。如果需要使用Lazy方式,可以使用sequenceOf或asSequence,序列都是使用的惰性初始化。
Map default
val map = mapOf<Any, Any>().withDefault { "default" }
println(map["1"])
-
a) default -
b) nothing -
c) null -
d) will not compile*
答案:C
不要被withDefault的字面意义骗了,withDefault只能用于委托属性的场景,所以,不知道的拓展函数,一定要进去看下实现,不能瞎猜。
val map = mutableMapOf<String, Set<String>>().withDefault { mutableSetOf() }
var property: Set<String> by map // returns empty set by default
Null empty
val s: String? = null
if (s?.isEmpty()) println("is empty")
if (s.isNullOrEmpty()) println("is null or empty")
-
a) is empty is null or empty -
b) is null or empty -
c) prints nothing -
d) doesn’t compile
答案:D
当s == null时,s?.isEmpty()会返回null,所以,这个表达式的返回类型应该是Boolean?,所以,不能编译通过。可以通过下面的方式进行修改:
val s: String? = null
if (s?.isEmpty() == true) {
println("is empty")
}
if (s.isNullOrEmpty()) {
println("is null or empty")
}
List or not
val x = listOf(1, 2, 3)
println(x is List<*>)
println(x is MutableList<*>)
println(x is java.util.List<*>)
-
a) true false true -
b) false false true -
c) true true true -
d) true false false
答案:C
在Kotlin中,listOf、MutableList、Java ArrayList,返回的都是java.util.List,所以它们的类型是一样的。
Everything is mutable
val readonly = listOf(1, 2, 3)
if (readonly is MutableList) {
readonly.add(4)
}
println(readonly)
-
a) [1, 2, 3] -
b) [1, 2, 3, 4] -
c) UnsupportedOperationException -
d) Will not compile
答案:C
类似listOf、Array.asList()这样的Helper functions它们返回的是java.util.Arrays$ArrayLis,而不是java.util.ArrayList,所以,他们是不能修改的。
Fun with composition
val increment = { i: Int -> i + 1 }
val bicrement = { i: Int -> i + 2 }
val double = { i: Int -> i * 2 }
val one = { 1 }
private infix fun <T, R> (() -> T).then(another: (T) -> R): () -> R = { another(this()) }
operator fun <T, R1, R2> ((T) -> R1).plus(another: (T) -> R2) = { x: T -> this(x) to another(x) }
调用:
val equilibrum = one then double then (increment + bicrement)
println(equilibrum())
-
a) Nothing, it doesn’t compile -
b) 5 -
c) (1, 2) -
d) (3, 4)
答案:D
这是一个经典的复合函数问题,重载Plus函数返回了一个又两个函数生成的pair,所以,我们都{1}开始,通过then中缀运算符double了,变成了2,在plus中,分别被执行为3和4。
Sorting
val list = arrayListOf(1, 5, 3, 2, 4)
val sortedList = list.sort()
println(sortedList)
-
a) [1, 5, 3, 2, 4] -
b) [1, 2, 3, 4, 5] -
c) kotlin.Unit -
d) Will not compile
答案:C
这道题考察的是Kotlin中sort函数,它有两种:
-
sort():对一个可变集合进行排序,返回Unit -
sorted():排序后返回集合
所以,换成list.sorted()就对了。
Collection equality
println(listOf(1, 2, 3) == listOf(1, 2, 3))
println(listOf(1, 2, 3).asSequence() == listOf(1, 2, 3).asSequence())
println(sequenceOf(1, 2, 3) == sequenceOf(1, 2, 3))
-
a) true; true; true -
b) true; true; false -
c) true; false; true -
d) true; false; false -
e) false; false; false
答案:D
集合的相等判断使用的是引用判断,所以两个不同的list,不会相等,sequence也一样,判断的是引用地址。
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(", ")
print(d.sum(x = 0))
-
a) 2,2 -
b) 1,1 -
c) 2,1 -
d) Will not compile
答案:C
这道题的考察点主要是下面几个:
-
open functions的多态的,其类型有jvm虚拟机在运行时决定 -
具名函数是静态的,在编译期就固定了
Overriding properties that are used in a parent
open class Parent(open val a: String) {
init { println(a) }
}
class Children(override val a: String): Parent(a)
调用:
Children("abc")
-
a) abc -
b) Unresolved reference: a -
c) Nothing, it won’t compile -
d) null
答案:D
这个问题是Kotlin implementing的一个比较让人困扰的地方,所以,我们来分析下Kotlin生成的Java代码。
public static class Parent {
private final String a;
public String getA() {
return this.a;
}
Parent(String a) {
super();
this.a = a;
System.out.print(this.getA());
}
}
public static final class Children extends Parent {
private final String a;
public String getA() {
return this.a;
}
Children(String a) {
super(a);
this.a = a;
}
}
❝As you can see, to get a we use
❞getA
method which referencesa
. The only problem is that it is overriten inChild
so it actually referencesa
fromChild
which is not set yet at this point. It is because parent is always initialized first.
可以看见,Parent中的a,在Child中被重写了,所以它实际上引用了Child中的a,而这个a在此时还没有被设置,因为父类总是先被初始化。所以,在使用Kotlin的简化构造函数时,一定要注意属性的覆写。
Child apply
open class Node(val name: String) {
fun lookup() = "lookup in: $name"
}
class Example : Node("container") {
fun createChild(name: String): Node? = Node(name)
val child1 = createChild("child1")?.apply {
println("child1 ${lookup()}")
}
val child2 = createChild("child2").apply {
println("child2 ${lookup()}")
}
}
调用:
Example()
-
A) child1 lookup in: child1; child2 lookup in: child2 -
B) child1 lookup in: child1; child2 lookup in: container -
C) child1 lookup in: container; child2 lookup in: child2 -
D) none of the above
答案:B
由于createChild返回nullable,所以在child2的apply中,我们收到的context是Node?。我们不能在没有unpack的情况下直接调用lookup。如果我们想这样做,我们应该使用this?.lookup()。由于我们没有这样做,编译器会搜索它可以使用的lookup,并在Example上下文中找到它的实现。
Negative numbers
print(-1.inc())
print(", ")
print(1 + -(1))
-
a) 0, 0 -
b) Won’t compile in line 4 -
c) 0, 2 -
d) -2, 0
答案:D
在这两种情况下,我们在Int类型上使用unaryMinus操作。当你输入-1时,它与1.unaryMinus()相同。这就是为什么1 + -(1)能正确工作。-1.inc()返回-2,因为inc用在了运算符之前。这个表达式等同于1.inc().unaryMinus()。为了解决这个问题,你应该使用小括号(-1).inc()。
Copy
data class Container(val list: MutableList<String>)
val list = mutableListOf("one", "two")
val c1 = Container(list)
val c2 = c1.copy()
list += "oops"
println(c2.list.joinToString())
-
a) one, two -
b) one, two, oops -
c) UnsupportedOperationException -
d) will not compile
答案:B
data class的copy()方法只做了一个浅层拷贝,即只复制了对字段的引用。如果要实现深拷贝,可以使用不可变data class来避免这个问题。
Covariance
class Wrapper<out T>
val instanceVariableOne : Wrapper<Nothing> = Wrapper<Any>()//Line A
val instanceVariableTwo : Wrapper<Any> = Wrapper<Nothing>()//Line B
-
a) Both lines A and B compile -
b) Lines A and B do not compile -
c) Line A compiles; Line B does not -
d) Line B compiles; Line A does not
答案:D
这道题考察的是kotlin的协变,Wrapper
Receivers wars
fun foo() {
println("Top-level rule")
}
class Foo {
fun foo() {
println("Extension receiver rule")
}
}
class Test {
fun foo() {
println("Dispatch receiver rule")
}
fun Foo.foo() {
println("Member extension function rule")
}
fun Foo.test() {
foo()
}
fun testFoo() {
Foo().test()
}
}
调用:
Test().testFoo()
-
a) Top-level rule -
b) Extension receiver rule -
c) Dispatch receiver rule -
d) Member extension function rule
答案:B
当我们有一个extension receiver (Foo)时,它的方法总是比dispatch receiver(同一类中的方法)有更高的优先级。
而当Member extension和extension receiver冲突时,extension receiver一定会被调用,所以Member extension的优先级是最低的。
Int plus-plus
var i = 0
println(i.inc())
println(i.inc())
var j = 0
println(j++)
println(++j)
-
a) 0, 1, 0, 1 -
b) 0, 1, 0, 2 -
c) 1, 1, 0, 2 -
d) 1, 2, 0, 1
答案:C
这个问题从C++就开始存在了,又想起了谭浩强的支配。前缀运算符++(++j)增加数字并返回新值,后缀运算符也增加属性,但返回前值。
但会令人疑惑的部分是,前缀和后缀都是对Kotlin函数inc的引用,你从ide中点击++i和i++,都会跳到inc的引用,inc返回了一个新值,但是未被赋值。
Return in function literal
fun f1() {
(1..4).forEach {
if (it == 2) return
println(it)
}
}
fun f2() {
(1..4).forEach(
fun(it) {
if (it == 2) return
println(it)
})
}
调用:
f1()
f2()
-
a) 134134 -
b) 1134 -
c) 1341 -
d) Doesn’t compile
答案:B
当我们想在lambda表达式中使用return时,我们需要使用return@forEach这样的标签,否则它会跳出整个lambda。
而因为for-each是内联函数,所以在f2中,实际上使用了一个匿名函数,这里return就可以退出函数,而不是lambda。
WTF with labels
val j = wtf@ { n: Int -> wtf@ (wtf@ n + wtf@ 2) }(10)
println(j)
-
a) It won’t compile -
b) 10 -
c) 2 -
d) 12
答案:D
标签在这里毫无作用,不要被他迷惑了。
Order of nullable operators
val x: Int? = 2
val y: Int = 3
val sum = x?:0 + y
println(sum)
-
a) 3 -
b) 5 -
c) 2 -
d) 0
答案:C
Elvis operator的优先级比+低,所以加号先被执行,就变成了x?:3,答案是2,可以通过加括号的方式(x3:0)来改变优先级。
Extended enums
enum class Color {
Red, Green, Blue
}
fun Color.from(s: String) = when (s) {
"#FF0000" -> Color.Red
"#00FF00" -> Color.Green
"#0000FF" -> Color.Blue
else -> null
}
调用:
println(Color.from("#00FF00"))
-
a) Green -
b) Color.Green -
c) null -
d) will not compile
答案:D
对Color的扩展函数只适用于Color的实例,例如,Color.Blue.from(),对枚举本身的扩展函数只有在它有一个Companion object时才能进行。
enum class Color {
Red, Green, Blue;
companion object
}
fun Color.Companion.from(...)
这又是一个骚操作。
Hello blocks
fun hello(block: String.() -> Unit) {
"Hello1".block()
block("Hello2")
}
调用:
hello { println(this) }
-
a) Hello1 -
b) Hello2 -
c) Hello1Hello2 -
d) will not compile
答案:C
这道题的重点是分清楚哪个是lambda,哪个是带接收器的拓展函数。
I am this
data class IAm(var foo: String) {
fun hello() = foo.apply {
return this
}
}
调用:
println(IAm("bar").hello())
-
a) IAm -
b) IAm(foo=bar) -
c) bar -
d) Will not compile
答案:C
不要被迷惑了,这就是一段废代码。
Overextension
operator fun String.invoke(x: () -> String) = this + x()
fun String.z() = "!$this"
fun String.toString() = "$this!"
调用:
println("x"{"y"}.z())
-
a) !x -
b) !xy -
c) !xy! -
d) Will not compile
答案:B
这道题重点是理清"x"{"y"}.z(),去掉z(),实际上就是重载的invoke函数,所以等价于String{},{}就是invoke的参数。
又是一个骚操作,可以在对象初始化的时候进行其它初始化操作。
Lazy delegate
class Lazy {
var x = 0
val y by lazy { 1 / x }
fun hello() {
try {
print(y)
} catch (e: Exception) {
x = 1
print(y)
}
}
}
调用:
Lazy().hello()
-
a) 0 -
b) 1 -
c) NaN -
d) ArithmeticException
答案:B
Lazy delegate可以被多次调用,直到它真正返回一个值为止,所以抛出异常后,x的值修改了,y可以被赋值,从而print出来。
Sneaky return
fun numbers(list: List<Int>) {
list.forEach {
if (it > 2) return
println(it)
}
println("ok")
}
调用:
numbers(listOf(1, 2, 3))
-
a) 123ok -
b) 12ok -
c) 12 -
d) Infinite loop
答案:C
lambda中的return,会直接从函数中返回,所以函数中断了。
Two lambdas
typealias L = (String) -> Unit
fun foo(one: L = {}, two: L = {}) {
one("one")
two("two")
}
调用:
foo { println(it) }
foo({ println(it) })
-
a) oneone -
b) twotwo -
c) onetwo -
d) none of the above -
e) none of the above (twoone)
答案:E
这道题搞清楚了,lambda就算是真的搞清楚了,foo {},代表的是lambda省略()的写法,{}实际上是foo的最后一个参数,而foo(),括号中的内容,实际上是foo中按顺序的第一个参数。
-
这对DSL来说是非常好的,可以通过Kotlin完成各种DSL的写法 -
但是当与默认参数结合在一起时,可能会引起混淆,不要把许多lambda作为参数,如果你仍然这样做,要避免使用默认值
❝案例来自于Puzzlers on Kt. Academy
❞
大奖
var reward: 大奖? = null
很多人说,这些玩意儿到底有啥用,很多代码放IDE里面就能知道到底是对是错,运行结果是什么,为什么还要这样去做呢?
实际上,理解这些东西,对你的编程思维和对语言的理解能力会有很大帮助,在IDE里面,它帮助我们做了太多的事,以至于我们很多时候都不能真正发现问题的本质是什么,借助这些题目的训练,我们可以理解编译器是如何处理代码的,可以理解代码是如何执行的,这才是我们训练这些题目的目的。
so,这次鱿鱼游戏,你活到最后了吗?
向大家推荐下我的网站 https://xuyisheng.top/ 点击原文一键直达
专注 Android-Kotlin-Flutter 欢迎大家访问
往期推荐
更文不易,点个“三连”支持一下👇