Kotlin魷魚游戲大獎賽
點擊上方藍字關(guān)注我,知識會給你力量
魷魚游戲來了,現(xiàn)在開始,看看你闖過第幾關(guān)。
在不借助IDE的情況下,看你的人肉編譯器能否編譯出正確的結(jié)果。
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
要執(zhí)行這個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是由一個三引號("")定義的,不包含轉(zhuǎn)義,但可以包含換行符和任何其他字符。即使有轉(zhuǎn)義字符\, 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結(jié)構(gòu)實際上只是用一個"單行"if調(diào)用else來處理的,也就是說,不管有多少個else if,實際是都會轉(zhuǎn)化為else中的嵌套。在Kotlin中,函數(shù)在if-else塊之前被解析,所以.let { print(it) }只適用于最后的else if。所以在這種情況下,第一個if語句的結(jié)果將不會被使用,函數(shù)將立即返回。為了避免這種情況,你可以將整個if ... else ... 包裹在小括號中,然后在其上加上.let。
Lambda runnables
fun run() {
val run: () -> Unit = {
println("Run run run!")
}
Runnable { run() }.run()
}
調(diào)用:
run()
-
a) “Run run run!” -
b) Doesn’t compile -
c) StackOverflowError -
d) None of the above
答案:A
這道題實際上是考察的Kotlin局部函數(shù)的使用,上面的代碼,實際上等價于下面的代碼:
val run1: () -> Unit = {
println("Run run run!")
}
fun run() {
Runnable { run1() }.run()
}
使用局部函數(shù),可以將邏輯隱藏在函數(shù)內(nèi)部。
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(chǎn)’ overrides nothing -
d) Error: Function ‘a(chǎn)’ must have a body
答案:B
我們可以用抽象的函數(shù)來覆寫一個open的函數(shù),但這樣它還是抽象的,我們需要在所有子類中覆寫需要實現(xiàn)的方法,像下面這樣的代碼就可以執(zhí)行了。
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函數(shù)的實現(xiàn),在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
操作符重載函數(shù)plus的定義是完全正確的。它返回新的函數(shù)(使用lambda表達式創(chuàng)建),該函數(shù)由兩個作為參數(shù)的函數(shù)組成。當(dāng)我們添加兩個函數(shù)時,我們就有了另一個可以調(diào)用的函數(shù)。當(dāng)我們調(diào)用它時,我們有一個接一個的lambda表達式被調(diào)用。
What am I?
val whatAmI = {}()
println(whatAmI)
-
a) “null” -
b) “kotlin.Unit” -
c) Doesn’t print anything -
d) Doesn’t compile
答案:B
這道題考察的是lambda表達式的基本知識,這個lambda表達式?jīng)]有返回內(nèi)容,所以它的類型就是Unit。
Return return
fun f1(): Int {
return return 42
}
fun f2() {
throw throw Exception()
}
f1和f2能執(zhí)行嗎?
-
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中,它也以結(jié)果42結(jié)束f1的執(zhí)行。同樣地,throw聲明類型——Nothing也是一個返回類型,所以兩個函數(shù)都能編譯,但是在f2調(diào)用的時候,會以異常結(jié)束。
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())
}
調(diào)用:
printFoo(D())
-
a) Doesn’t compile -
b) Runtime error -
c) c -
d) d
答案:C
這個例子考察的是拓展函數(shù)的具體實現(xiàn)原理,因為被調(diào)用的擴展函數(shù)只依賴于參數(shù)c的聲明類型,也就是C類,所以,它只會調(diào)用C類的拓展函數(shù)foo。
擴展實際上并不會修改它們所擴展的類。通過定義一個擴展函數(shù),你并沒有真實的在一個類中插入新的成員,而只是讓新的函數(shù)可以在這個類型的變量上用點號來調(diào)用,相當(dāng)于一層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的結(jié)果分別是什么?
-
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中我們實際上是實現(xiàn)了一個匿名函數(shù),所以輸出一個函數(shù)。
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中的集合擴展函數(shù)是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只能用于委托屬性的場景,所以,不知道的拓展函數(shù),一定要進去看下實現(xiàn),不能瞎猜。
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
當(dāng)s == null時,s?.isEmpty()會返回null,所以,這個表達式的返回類型應(yīng)該是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) }
調(diào)用:
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
這是一個經(jīng)典的復(fù)合函數(shù)問題,重載Plus函數(shù)返回了一個又兩個函數(shù)生成的pair,所以,我們都{1}開始,通過then中綴運算符double了,變成了2,在plus中,分別被執(zhí)行為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函數(shù),它有兩種:
-
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)
}
調(diào)用:
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的多態(tài)的,其類型有jvm虛擬機在運行時決定 -
具名函數(shù)是靜態(tài)的,在編譯期就固定了
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)
調(diào)用:
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
?getAmethod which referencesa. The only problem is that it is overriten inChildso it actually referencesafromChildwhich is not set yet at this point. It is because parent is always initialized first.
可以看見,Parent中的a,在Child中被重寫了,所以它實際上引用了Child中的a,而這個a在此時還沒有被設(shè)置,因為父類總是先被初始化。所以,在使用Kotlin的簡化構(gòu)造函數(shù)時,一定要注意屬性的覆寫。
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()}")
}
}
調(diào)用:
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的情況下直接調(diào)用lookup。如果我們想這樣做,我們應(yīng)該使用this?.lookup()。由于我們沒有這樣做,編譯器會搜索它可以使用的lookup,并在Example上下文中找到它的實現(xiàn)。
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操作。當(dāng)你輸入-1時,它與1.unaryMinus()相同。這就是為什么1 + -(1)能正確工作。-1.inc()返回-2,因為inc用在了運算符之前。這個表達式等同于1.inc().unaryMinus()。為了解決這個問題,你應(yīng)該使用小括號(-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()方法只做了一個淺層拷貝,即只復(fù)制了對字段的引用。如果要實現(xiàn)深拷貝,可以使用不可變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的協(xié)變,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()
}
}
調(diào)用:
Test().testFoo()
-
a) Top-level rule -
b) Extension receiver rule -
c) Dispatch receiver rule -
d) Member extension function rule
答案:B
當(dāng)我們有一個extension receiver (Foo)時,它的方法總是比dispatch receiver(同一類中的方法)有更高的優(yōu)先級。
而當(dāng)Member extension和extension receiver沖突時,extension receiver一定會被調(diào)用,所以Member extension的優(yōu)先級是最低的。
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)增加數(shù)字并返回新值,后綴運算符也增加屬性,但返回前值。
但會令人疑惑的部分是,前綴和后綴都是對Kotlin函數(shù)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)
})
}
調(diào)用:
f1()
f2()
-
a) 134134 -
b) 1134 -
c) 1341 -
d) Doesn’t compile
答案:B
當(dāng)我們想在lambda表達式中使用return時,我們需要使用return@forEach這樣的標(biāo)簽,否則它會跳出整個lambda。
而因為for-each是內(nèi)聯(lián)函數(shù),所以在f2中,實際上使用了一個匿名函數(shù),這里return就可以退出函數(shù),而不是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
標(biāo)簽在這里毫無作用,不要被他迷惑了。
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的優(yōu)先級比+低,所以加號先被執(zhí)行,就變成了x?:3,答案是2,可以通過加括號的方式(x3:0)來改變優(yōu)先級。
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
}
調(diào)用:
println(Color.from("#00FF00"))
-
a) Green -
b) Color.Green -
c) null -
d) will not compile
答案:D
對Color的擴展函數(shù)只適用于Color的實例,例如,Color.Blue.from(),對枚舉本身的擴展函數(shù)只有在它有一個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")
}
調(diào)用:
hello { println(this) }
-
a) Hello1 -
b) Hello2 -
c) Hello1Hello2 -
d) will not compile
答案:C
這道題的重點是分清楚哪個是lambda,哪個是帶接收器的拓展函數(shù)。
I am this
data class IAm(var foo: String) {
fun hello() = foo.apply {
return this
}
}
調(diào)用:
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!"
調(diào)用:
println("x"{"y"}.z())
-
a) !x -
b) !xy -
c) !xy! -
d) Will not compile
答案:B
這道題重點是理清"x"{"y"}.z(),去掉z(),實際上就是重載的invoke函數(shù),所以等價于String{},{}就是invoke的參數(shù)。
又是一個騷操作,可以在對象初始化的時候進行其它初始化操作。
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)
}
}
}
調(diào)用:
Lazy().hello()
-
a) 0 -
b) 1 -
c) NaN -
d) ArithmeticException
答案:B
Lazy delegate可以被多次調(diào)用,直到它真正返回一個值為止,所以拋出異常后,x的值修改了,y可以被賦值,從而print出來。
Sneaky return
fun numbers(list: List<Int>) {
list.forEach {
if (it > 2) return
println(it)
}
println("ok")
}
調(diào)用:
numbers(listOf(1, 2, 3))
-
a) 123ok -
b) 12ok -
c) 12 -
d) Infinite loop
答案:C
lambda中的return,會直接從函數(shù)中返回,所以函數(shù)中斷了。
Two lambdas
typealias L = (String) -> Unit
fun foo(one: L = {}, two: L = {}) {
one("one")
two("two")
}
調(diào)用:
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的最后一個參數(shù),而foo(),括號中的內(nèi)容,實際上是foo中按順序的第一個參數(shù)。
-
這對DSL來說是非常好的,可以通過Kotlin完成各種DSL的寫法 -
但是當(dāng)與默認參數(shù)結(jié)合在一起時,可能會引起混淆,不要把許多l(xiāng)ambda作為參數(shù),如果你仍然這樣做,要避免使用默認值
?案例來自于Puzzlers on Kt. Academy
?
大獎
var reward: 大獎? = null
很多人說,這些玩意兒到底有啥用,很多代碼放IDE里面就能知道到底是對是錯,運行結(jié)果是什么,為什么還要這樣去做呢?
實際上,理解這些東西,對你的編程思維和對語言的理解能力會有很大幫助,在IDE里面,它幫助我們做了太多的事,以至于我們很多時候都不能真正發(fā)現(xiàn)問題的本質(zhì)是什么,借助這些題目的訓(xùn)練,我們可以理解編譯器是如何處理代碼的,可以理解代碼是如何執(zhí)行的,這才是我們訓(xùn)練這些題目的目的。
so,這次魷魚游戲,你活到最后了嗎?
向大家推薦下我的網(wǎng)站 https://xuyisheng.top/ 點擊原文一鍵直達
專注 Android-Kotlin-Flutter 歡迎大家訪問
往期推薦
更文不易,點個“三連”支持一下??
