科特林拼图,卷。 2:新一批难题



您可以预测这样的Kotlin代码如何表现吗? 它会编译输出什么,为什么?

不管编程语言有多好,它都会抛出它,以至于它只会擦伤后脑。 Kotlin也不例外-当即使很短的一段代码都具有意外行为时,它也包含困惑器。

早在2017年,我们就在Habré上发布了Anton Keks antonkeks 精选的此类益智游戏 。 后来,他在Mobius上与我们一起进行了第二次选择,我们现在也将其翻译成Habr转换为文本视图,将正确的答案隐藏在剧透下面。

我们还会附上演讲的视频录像,如果文本中出现难以理解的内容,您也可以与她联系。


上半部分的谜题针对那些对Kotlin不太熟悉的人。 下半部分是针对Kotlin核心开发人员的。 即使启用了渐进模式,我们也将在Kotlin 1.3上启动所有功能。 Puzzler源代码在GitHub上 。 谁提出新的想法,发送请求请求。

1号观众


fun hello(): Boolean { println(print(″Hello″) == print(″World″) == return false) } hello() 

在我们面前的是一个简单的hello函数,它会运行几次打印。 然后我们自己启动此功能。 一个简单的超频问题:应该显示什么?

a)HelloWorld
b)HelloWorldfalse
c)HelloWorldtrue
d)未编译

正确答案


第一种选择是正确的。 两种打印都已经开始后才触发比较,但无法更早开始。 为什么要完全编译这样的代码? 除了返回Nothing以外的任何函数都将返回某些内容。 由于Kotlin中的所有内容都是一个表达式,所以即使return也是一个表达式。 return的返回类型为Nothing,它强制转换为任何类型,因此您可以像这样进行比较。 并且打印返回Unit,因此Unit可以多次与Nothing进行比较,并且一切正常。

观众人数2


 fun printInt(n: Int) { println(n) } printInt(-2_147_483_648.inc()) 

提示,您不会猜到:可怕的数字实际上是最小的32位有符号整数。

这里的一切看起来都很简单。 Kotlin具有出色的扩展功能,例如.inc()可以递增。 我们可以在Int上调用它,并且可以打印结果。 会发生什么?

a)-2147483647
b)-2147483649
c)2147483647
d)以上都不是

发射!


从错误消息中可以看到,这是Long的问题。 但是为什么要长呢?

扩展函数具有优先权,编译器首先运行inc(),然后运行减号运算符。 如果inc()被删除,则它将为Int,并且一切正常。 但是inc()首先开始将2_147_483_648转换为Long,因为此数字不带减号不再是有效的Int。 原来是Long,只有这样才叫减号。 所有这些都不能再传递给printInt()函数,因为它需要一个Int。

如果将对printInt的调用更改为可以接受Long的常规打印,则第二个选项将是正确的。



我们看到这实际上是Long。 请注意:并非所有的拼图游戏都可以在真实代码中运行,但是这个可以。

观众人数3


 var x: UInt = 0u println(x--.toInt()) println(--x) 

Kotlin 1.3中引入了新的强大功能。 除了Corutin的最终版本,我们
现在终于有了未签名的数字。 这是必要的,尤其是在编写某种网络代码时。

现在,对于文字,甚至还有一个特殊的字母u,我们可以定义常量,如示例中所示,我们可以将x减1并转换为Int。 我提醒您Int熟悉我们。

会发生什么?

a)-1 4294967294
b)0 4294967294
c)0 -2
d)未编译

4294967294是可以获取的最大32位数字。

发射!


更正选项b。

在这里,与以前的版本一样:首先,在x上调用toInt(),然后才递减。 显示无符号减量的结果,这是unsignedInt的最大值。

最有趣的是,如果您这样编写,代码将无法编译:

 println(x--.toInt()) println(--x.toInt()) 

对于我来说,第一行有效,而第二行有效-这是不合逻辑的,这很奇怪。

在预发行版本中,正确的选项是C,因此在JetBrains中做得很好,可以在发行最终版本之前修复错误。

观众人数4


 val cells = arrayOf(arrayOf(1, 1, 1), arrayOf(0, 1, 1), arrayOf(1, 0, 1)) var neighbors = cells[0][0] + cells[0][1] + cells[0][2] + cells[1][0] + cells[1][2] + cells[2][0] + cells[2][1] + cells[2][2] print(neighbors) 

我们在实际代码中遇到了这种情况。 我们在Codeborne进行了Coding Dojo编码,并在Kotlin 的《生命游戏》中实现了它。 如您所见,在Kotlin上使用多级阵列并不是很方便。

在“生命游戏”中,算法的重要部分是确定一个小区的邻居数量。 周围的所有小动物都是邻居,这取决于细胞是存活还是死亡。 在这段代码中,您可以数一数并假设会发生什么。

a)6
b)3
c)2
d)未编译

让我们来看看


正确答案是3。

事实是第一行的加号向下移动,而Kotlin认为这是一元加号()。 结果,仅对前三个单元进行求和。 如果要分几行编写此代码,则需要向上移动加号。

这是另一个“坏难题”。 请记住,在Kotlin中,您不需要将语句转移到新行,否则它可能会认为它是一元的。



我没有看到在DSL之外的实际代码中需要unaryPlus的情况。 这是一个非常奇怪的话题。

这是没有分号时我们要付出的代价。 如果是这样,则很清楚一个表达式何时结束而另一个表达式何时开始。 没有它们,编译器必须做出决定。 编译器的换行符通常意味着尝试单独检查这些行是有意义的。

但是有一种非常酷的JavaScript语言,您也无法使用其中的分号编写该代码,但该代码仍然可以正常工作。

观众人数5


 val x: Int? = 2 val y: Int = 3 val sum = x?:0 + y println(sum) 

该拼图游戏由KotlinConf演讲者Thomas Nild主讲。

Kotlin具有强大的可空类型功能。 我们有可为空的x,如果结果为null,则可以通过Elvis运算符将其转换为某个正常值。

会发生什么?

a)3
b)5
c)2
d)0

发射!


问题再次出在操作员的顺序或优先级上。 如果我们重新设置格式,则官方格式将执行以下操作:

 val sum = x ?: 0+y 

该格式已经建议0 + y首先开始,然后才是x?:。 因此,当然还有2个,因为X为2,所以它不为null。

观众人数6


 data class Recipe (var name: String? = null, var hops: List<Hops> = emptyList() ) data class Hops(var kind: String? = null, var atMinute: Int = 0, var grams: Int = 0) fun beer(build: Recipe.() -> Unit) = Recipe().apply(build) fun Recipe.hops(build: Hops.() -> Unit) { hops += Hops().apply(build) } val recipe = beer { name = ″Simple IPA″ hops { name = ″Cascade″ grams = 100 atMinute = 15 } } 

当他们打电话给我时,他们答应我精酿啤酒。 我今晚要去找他,还没见过他。 Kotlin有一个很棒的话题-建筑商。 使用四行代码,我们编写了DSL,然后通过构建器创建了DSL。

我们首先创建IPA,然后在沸腾的第15分钟内添加称为Cascade的啤酒花,100克,然后打印此配方。 我们做了什么?

a)配方(名称=简单IPA,跃点= [跃点(名称=级联,atMinute = 15,克= 100)])
b)IllegalArgumentException
c)未编译
d)以上都不是

发射!


我们得到了与精酿啤酒类似的东西,但是里面没有啤酒花,它消失了。 他们想要IPA,但得到了波罗的海7。

这就是命名冲突发生的地方。 Hops中的字段实际上称为kind,在行名称=“ Cascade”中,我们使用name,该名称与配方名称一起缓存。

我们可以创建自己的BeerLang批注并将其注册为BeerLang DSL的一部分。 现在,我们正在尝试运行此代码,不应与我们一起对其进行编译。



现在我们被告知,原则上不能在此上下文中使用名称。 为此,需要DSLMarker,因为构建器内部的编译器不允许我们使用外部字段,如果内部具有相同的字段,则不存在命名冲突。 像这样固定代码,我们得到了食谱。



观众人数7



 fun f(x: Boolean) { when (x) { x == true -> println(″$x TRUE″) x == false -> println(″$x FALSE″) } } f(true) f(false) 

这个难题是JetBrains的雇员之一。 Kotlin具有when功能。 它适用于所有场合,它允许您编写出色的代码,它通常与密封类一起用于API设计。

在这种情况下,我们有一个函数f(),该函数采用布尔值并根据true和false打印一些内容。

会发生什么?

a)是TRUE; 错误错误
b)是TRUE; 假TRUE
c)真假; 错误错误
d)以上都不是

让我们来看看


为什么这样 首先,我们计算表达式x == true:例如,在第一种情况下,它将为true == true,这表示true。 然后还与我们在何时传递的模式进行了比较。

当x设置为false时,将x == true评估为false,但是样本也将为false-因此示例将与样本匹配。

有两种方法可以修复此代码,一种是在两种情况下都删除“ x ==”:

 fun f(x: Boolean) { when (x) { true -> println(″$x TRUE″) false -> println(″$x FALSE″) } } f(true) f(false) 

第二种选择是在时间之后删除(x)。 当在任何条件下均可工作时,则将与样品不匹配。

 fun f(x: Boolean) { when { x == true -> println(″$x TRUE″) x == false -> println(″$x FALSE″) } } f(true) f(false) 


观众八号


 abstract class NullSafeLang { abstract val name: String val logo = name[0].toUpperCase() } class Kotlin : NullSafeLang() { override val name = ″Kotlin″ } print(Kotlin().logo) 

Kotlin被推销为“零安全”语言。 想象一下,我们有一个抽象类,它有一个名称,还有一个返回该语言徽标的属性:以防万一,该名称的首字母大写(突然被忘了使用大写字母)。

由于该语言是无效的语言,因此我们将更改名称,并且可能应该获得正确的徽标,即一个字母。 我们真正得到什么?

a)K
b)NullPointerException
c)IllegalStateException
d)未编译

发射!


我们收到了一个N​​ullPointerException,我们不应该收到它。 问题在于,首先调用超类的构造函数,代码尝试初始化属性徽标,并从零开始使用名称char,此时名称为null,因此发生NullPointerException。

解决此问题的最佳方法是:

 class Kotlin : NullSafeLang() { override val name get() = ″Kotlin″ } 

如果运行这样的代码,则得到“ K”。 现在,基类将调用基类的构造函数,它实际上将调用getter名称并获取Kotlin。

属性是Kotlin的一项重要功能,但是您在覆盖属性时需要非常小心,因为它很容易忘记,犯错或确保做错事情。


9号观众


 val result = mutableListOf<() -> Unit>() var i = 0 for (j in 1..3) { i++ result += { print(″$i, $j; ″) } } result.forEach { it() } 

有一些令人恐惧的事情的可变列表。 如果它使您想起Scala,那么它并不是徒劳的,因为它的确看起来像。 有一个List lambd,我们有两个计数器-I和j,递增,然后对它们执行一些操作。 会发生什么?

a)1 1; 2 2; 3 3
b)1 3; 2 3; 3 3
c)3 1; 3 2; 3 3
d)以上都不是

跑吧


我们得到3 1; 3 2; 3 3.发生这种情况是因为i是一个变量,它将一直保留其值直到函数结束。 j通过值传递。

如果不是var i = 0,而是val i = 0,这将不起作用,但是我们不能递增变量。

在Kotlin中,我们使用闭包,而Java中没有此功能。 这很酷,但是如果我们不立即使用i的值,而是将其传递给lambda,它会在以后给我们带来麻烦,该lambda会在以后启动并查看此变量的最后一个值。 并且j通过值传递,因为循环条件中的变量-它们与val相同,因此不再更改其值。

在JavaScript中,答案将是“ 3 3; 3 3; 3 3”,因为没有值传输。


观众人数10


 fun foo(a:Boolean, b: Boolean) = print(″$a, $b″) val a = 1 val b = 2 val c = 3 val d = 4 foo(c < a, b > d) 

我们有一个函数foo(),使用两个布尔值,将它们打印出来,一切似乎都很简单。 我们有很多数字,剩下的就是看看哪个数字比另一个数字大,然后确定哪个选项是正确的。

a)真实,真实
b)错误,错误
c)null,null
d)未编译

我们启动


未编译。

问题在于编译器认为这与通用参数相似:使用<a,b>。 尽管“ c”似乎不是一个类,但尚不清楚为什么它应该具有通用参数。

如果代码是这样的,那就可以了:

 foo(c > a, b > d) 

在我看来,这是编译器中的错误。 但是,当我带着任何这样的谜题去找安德烈·布雷斯拉夫(Andrei Breslav)时,他说:“这是因为解析器就是这样,他们不希望它太慢。” 通常,他总是找到原因的解释。

不幸的是,是这样。 他说他们不会修复它,因为解析器
Kotlin尚不了解语义。 解析首先发生,然后将其传递到另一个编译器组件。 不幸的是,这种情况可能仍然如此。 因此,不要在中间写两个这样的尖括号和任何代码!

观众人数11


 data class Container(val name: String, private val items: List<Int>) : List<Int> by items val (name, items) = Container(″Kotlin″, listOf(1, 2, 3)) println(″Hello $name, $items″) 

委托是Kotlin的一项重要功能。 顺便说一句,安德烈·布雷斯拉夫(Andrei Breslav)说,这是他很乐意从语言中删除的功能,他不再喜欢它。 现在,也许,我们将找出原因! 他还说,同伴的物体很丑陋。

但是数据类绝对是美丽的。 我们有一个数据类Container,它本身具有名称和项目。 同时,在Container中,我们实现项目的类型,即List,并将其所有方法委托给项目。

然后,我们使用另一个很酷的功能-分解。 我们从容器中“销毁”名称和项目元素,并将其显示在屏幕上。 一切似乎都很简单明了。 会发生什么?

a)你好科特林,[1,2,3]
b)你好,科特林,1岁
c)你好1、2
d)你好,科特林,2岁

我们启动


最模糊的选项是d。 他原来是真的。 事实证明,项目只是从项目集合中消失,而不是从开头或结尾消失,而只是在中间消失。 怎么了

销毁的问题是由于委托,科特林的所有藏品
有自己的解构选择。 我可以写val(I,j)= listOf(1,2),并将这1和2放入变量,即List已实现函数component1()和
component2()。

数据类还具有component1()和component2()。 但是,由于本例中的第二个组件是私有的,因此在List上公开的那个组件是赢家,因此第二个元素是从List上获得的,我们到了这里2。道德很简单:不要那样做,不要那样做。

观众人数12


下一个难题是非常可怕的。 这是一个与科特林有某种联系的顺从人,所以他知道自己在写什么。

 fun <T> Any?.asGeneric() = this as? T 42.asGeneric<Nothing>()!!!! val a = if (true) 87 println(a) 

我们在可为空的Any上具有扩展功能,也就是说,它可以完全应用于任何对象。 这是一个非常有用的功能。 如果尚不在您的项目中,则值得添加,因为它可以将您想要的一切放入任何东西。 然后我们取42并将其转换为Nothing。

好吧,如果我们想确保我们做了重要的事情,我们可以改为!!! 编写!!!!,Kotlin编译器允许您执行以下操作:如果您缺少两个感叹号,请至少编写26个。

然后我们做if(true),然后我自己什么都不知道。让我们立即选择会发生什么。

a)87
b)Kotlin.Unit
c)ClassCastException
d)未编译

看着


给出合理的解释是非常困难的。 最有可能的是,这里的部队是由于没有更多可推入的事实。 这是无效的代码,但是可以使用,因为我们没有使用过。 我们已经向Nothing上传了一些东西,这是一种特殊的类型,它告诉编译器该类型的实例永远都不会出现。 编译器知道,如果有出现Nothing的可能性(按定义是不可能的),则无法进一步检查,这是不可能的情况。

最有可能的是,这是编译器中的错误,JetBrains团队甚至表示,也许有一天该错误会得到修复,这不是很重要。 诀窍是因为这种转换,我们在这里欺骗了编译器。 如果删除行42.asGeneric <Nothing>()!!! 并停止作弊,代码将停止编译。 如果我们离开了,编译器就会发疯,认为这是一个不可能的表达式,然后把所有东西塞进去。

我明白 也许某天某人会更好地解释它。


观众13


我们有一个非常有趣的功能。 您可以使用依赖项注入,也可以不使用依赖项注入,通过对象进行单调运行,然后运行程序很酷。 为什么需要Koin,Dagger之类的东西? 但是,测试将很困难。

 open class A(val x: Any?) { override fun toString() = javaClass.simpleName } object B : A(C) object C : A(B) println(Bx) println(Cx) 

我们有一个开放给继承的类A,它继承了内部的东西,我们创建了两个对象,一个单例B和C,它们都从A继承并在那里相互传递。 即,形成了良好的循环。 然后我们打印出B和C得到的结果。

a)null; 空值
b)C; 空值
c)ExceptionInInitializerError
d)未编译

我们启动


正确的选项是C; 空值

一个人会认为,当第一个对象初始化时,第二个对象还不存在。 但是,当我们推论出这一点时,C缺少B。也就是说,获得了相反的顺序:由于某种原因,编译器决定先初始化C,然后再与C一起初始化B。这看起来很不合逻辑,相反,它是逻辑上为null ; B.

但是编译器试图做某事,但是没有成功,他在那儿留下了空,决定什么都不给我们。 也可能是这样。

如果有的话? 在参数类型中,删除?,则将无法使用。



我们可以向编译器说,当null解析后,他尝试了,但是失败了,但是呢? 不,他给我们抛出了一个例外,那就是不可能循环。

帕兹勒№14


1.3版在Kotlin中发布了很棒的新协程。 我想了很长时间,想出一个关于Corutin的谜题,以便有人可以理解。 我认为对于某些人来说,任何带有协程的代码都是一个难题。

在1.3版中,实验API中的某些函数名称已更改,在1.2版中。 例如,buildSequence()重命名为简单的sequence()。 也就是说,我们可以使用yield函数,无限循环来创建出色的序列,然后尝试从该序列中获取一些东西。

 package coroutines.yieldNoOne val x = sequence { var n = 0 while (true) yield(n++) } println(x.take(3)) 

他们用协程说,其他语言中所有很酷的原语(例如yield)都可以作为库函数来完成,因为yield是可以中断的暂停函数。

会发生什么?

a)[1、2、3]
b)[0,1,2]
c)无限循环
d)以上都不是

发射!


正确的选项是最后一个。

序列是一种懒惰的手段,当我们坚持下去时,它也是懒惰的。 但是,如果您添加toList,那么它将真正打印出[0,1,2]。

正确答案根本与协程无关。 协程确实有效,易于使用。 对于序列和收益函数,您甚至不需要将库与协程连接起来,所有内容都已在标准库中。

帕兹勒№15


JetBrains的开发人员也制服了这个难题。 有这样一个地狱般的代码:

 val whatAmI = {->}.fun Function<*>.(){}() println(whatAmI) 

当我第一次见到他时,在KotlinConf期间,我无法入睡,我试图了解那是什么。 这样的密码可以用Kotlin编写,因此如果有人认为Scalaz吓人,那么在Kotlin中也可以。

让我们猜测:

a)Kotlin.Unit
b)科特林
c)未编译
d)以上都不是

跑吧


我们得到了一个无处可来的部队。

怎么了 首先我们给变量lambda赋值:{->}-这是有效的代码,您可以编写一个空的lambda。 它没有参数,它什么也不返回。 因此,它返回Unit。

我们为该变量分配一个lambda,然后立即将扩展名写入该lambda,然后运行它。 实际上,它将仅保留Kotlin.Unit。

然后,您可以在此lambda上编写扩展功能:

 .fun Function<*>.(){} 

它在Function <*>类型上声明,而我们最上面的内容也适用于它。 实际上是Function <Unit>,但是我没有写出不清楚的Unit。 您知道科特林星号如何工作吗? , Java. , .

, Unit {}, , void-. , , . -, — .

. , Kotlin — . iOS- , , Kotlin !
Mobius, : Mobius 22-23 . Kotlin — «Coroutining Android Apps» . ( Android, iOS), — , 1 .

: , — 6 .

Source: https://habr.com/ru/post/zh-CN440974/


All Articles