Kotlin鱿鱼游戏大奖赛

共 27978字,需浏览 56分钟

 ·

2021-11-20 02:48













点击上方蓝字关注我,知识会给你力量

















鱿鱼游戏来了,现在开始,看看你闯过第几关。


在不借助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 references a. The only problem is that it is overriten in Child so it actually references a from Child 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
是Wrapper

的一个子类型,因为Nothing是Any的一个子类型。Wrapper的子类型与T的子类型相同。B行是好的。A行不能编译。它把超类型分配给一个子类型。


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 欢迎大家访问
















往期推荐














本文原创公众号:群英传,授权转载请联系微信(Tomcat_xu),授权后,请在原创发表24小时后转载。


< END >


作者:徐宜生




更文不易,点个“三连”支持一下👇





浏览 54
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报