Kotlin puzzlers, vol. 2: um novo lote de quebra-cabeças



Você pode prever como esse código Kotlin se comportará? Compilará o que será produzido e por quê?

Não importa o quão boa seja a linguagem de programação, ela pode ser lançada de modo que resta apenas arranhar a parte de trás da cabeça. Kotlin não é exceção - ele também contém quebra-cabeças, quando mesmo um pedaço muito curto de código tem um comportamento inesperado.

Em 2017, publicamos no Habré uma seleção desses quebra-cabeças de Anton Keks antonkeks . E depois, ele se apresentou conosco no Mobius com a segunda seleção, e agora também o traduzimos para Habr em uma exibição de texto, ocultando as respostas corretas sob os spoilers.

Também anexamos a gravação de vídeo do discurso, se algo parecer incompreensível no texto, você também poderá entrar em contato com ela.


A primeira metade dos quebra-cabeças é voltada para aqueles que não estão muito familiarizados com Kotlin; a segunda metade é para desenvolvedores hardcore do Kotlin. Iniciaremos tudo no Kotlin 1.3, mesmo com o modo progressivo ativado. Os códigos-fonte do quebra-cabeças estão no GitHub . Quem tiver novas idéias, envie solicitações pull.

Número Pazzler 1


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

Antes de nós, existe uma simples função hello, que executa várias impressões. E lançamos essa função em si. Uma pergunta simples sobre overclock: o que deve ser impresso?

a) HelloWorld
b) HelloWorldfalse
c) HelloWorldtrue
d) Não compilado

Resposta correta


A primeira opção estava correta. A comparação é acionada após o início das duas impressões, não é possível iniciar mais cedo. Por que esse código é compilado? Qualquer função diferente de retornar Nothing retorna algo. Como tudo em Kotlin é uma expressão, até o retorno também é uma expressão. O tipo de retorno de retorno é Nothing, é convertido para qualquer tipo, para que você possa comparar assim. E a impressão retorna a Unidade, para que a Unidade possa ser comparada a Nada inúmeras vezes, e tudo funciona muito bem.

Número Pazzler 2


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

Sugira que você não adivinha: um número assustador é realmente o menor número inteiro assinado de 32 bits possível.

Tudo parece simples aqui. O Kotlin possui ótimas funções de extensão, como .inc (), para incrementar. Podemos chamá-lo no Int e podemos imprimir o resultado. O que vai acontecer?

a) -2147483647
b) -2147483649
c) 2147483647
d) Nenhuma das opções acima

Lançamento!


Como você pode ver na mensagem de erro, aqui está o problema com o Long. Mas por quanto tempo?

As funções de extensão têm prioridade, e o compilador executa primeiro inc () e depois o operador menos. Se inc () for removido, será Int e tudo funcionará. Mas inc (), iniciando primeiro, transforma 2_147_483_648 em Long, porque esse número sem menos não é mais válido Int. Acontece que Long, e só então menos é chamado. Tudo isso não pode mais ser passado para a função printInt (), porque requer um Int.

Se alterarmos a chamada printInt para uma impressão regular, que pode aceitar Long, a segunda opção estará correta.



Vemos que isso é realmente longo. Cuidado com isso: nem todas as peças do quebra-cabeça podem ser encontradas em código real, mas essa pode.

Número Pazzler 3


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

No Kotlin 1.3, surgiram novos recursos excelentes. Além da versão final do corutin, nós também
agora finalmente têm números não assinados. Isso é necessário, especialmente se você estiver escrevendo algum tipo de código de rede.

Agora, para literais, existe até uma letra especial u, podemos definir constantes, podemos, como no exemplo, decrementar x e converter em Int. Lembro que Int está familiarizado conosco.

O que vai acontecer?

a) -1 4294967294
b) 0 4294967294
c) 0 -2
d) Não compilado

4294967294 é o número máximo de 32 bits que pode ser obtido.

Lançamento!


Opção correta b.

Aqui, como na versão anterior: primeiro, toInt () é chamado em x, e somente então diminui. O resultado do decremento não assinado é exibido e esse é o máximo de unsignedInt.

O mais interessante é que, se você escrever assim, o código não será compilado:

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

E para mim é muito estranho que a primeira linha funcione, e a segunda - não, isso é ilógico.

E na versão de pré-lançamento, a opção correta seria C, tão bem feita no JetBrains que corrige bugs antes do lançamento da versão final.

Número Pazzler 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) 

Nós encontramos este caso em código real. Nós da Codeborne criamos o Coding Dojo e o implementamos juntos no Kotlin Game of Life . Como você pode ver, não é muito conveniente trabalhar com matrizes de vários níveis no Kotlin.

Em Game of Life, uma parte importante do algoritmo é determinar o número de vizinhos para uma célula. Todos os pequenos ao redor são vizinhos e depende se a célula vive ou morre. Nesse código, você pode contar e assumir o que acontece.

a) 6
b) 3
c) 2
d) Não compilado

Vamos ver


A resposta correta é 3.

O fato é que o sinal de mais da primeira linha é movido para baixo, e Kotlin acha que isso é unaryPlus (). Como resultado, apenas as três primeiras células são somadas. Se quisermos escrever esse código em várias linhas, precisamos mover o sinal de mais.

Este é outro dos "maus quebra-cabeças". Lembre-se, no Kotlin, você não precisa transferir a declaração para uma nova linha, caso contrário, ela pode ser considerada unária.



Não vi situações em que o unaryPlus seja necessário em código real, exceto DSL. Este é um tópico muito estranho.

Esse é o preço que pagamos pela ausência de ponto e vírgula. Se fossem, ficaria claro quando uma expressão termina e outra começa. E sem eles, o compilador deve tomar a decisão. Alimentações de linha para o compilador muitas vezes significam que faz sentido tentar examinar as linhas separadamente.

Mas há uma linguagem JavaScript muito legal na qual você também não pode escrever ponto e vírgula, e esse código ainda funcionará corretamente.

Pazzler número 5


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

Este quebra-cabeças é apresentado pelo orador da KotlinConf, Thomas Nild.

O Kotlin possui um ótimo recurso de tipos anuláveis. Temos x anulável e podemos convertê-lo, se for nulo, através do operador Elvis para algum valor normal.

O que vai acontecer?

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

Lançamento!


O problema está novamente na ordem ou prioridade dos operadores. Se reformatarmos isso, o formato oficial fará o seguinte:

 val sum = x ?: 0+y 

O formato já sugere que 0 + y começa primeiro e somente então x?:. Portanto, é claro, 2 permanece, porque X é dois, não é nulo.

Número Pazzler 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 } } 

Quando me chamaram aqui, me prometeram cerveja artesanal. Vou procurá-lo hoje à noite, ainda não o vi. Kotlin tem um ótimo tópico - construtores. Com quatro linhas de código, escrevemos nossa DSL e depois a criamos através dos construtores.

Criamos, primeiramente, o IPA, adicionamos lúpulo chamado Cascade, 100 gramas no 15º minuto de cozimento, e depois imprimimos esta receita. O que nós fizemos?

a) Receita (nome = IPA simples, lúpulo = [Lúpulo (nome = Cascata, atMinuto = 15, gramas = 100)])
b) IllegalArgumentException
c) Não compilado
d) Nenhuma das opções acima

Lançamento!


Temos algo parecido com a cerveja artesanal, mas não há lúpulo nela, ela desapareceu. Eles queriam um IPA, mas conseguiram o Báltico 7.

É aqui que o conflito de nomes aconteceu. O campo no Hops é chamado de kind e, na linha name = ″ Cascade ″, usamos name, que é armazenado em cache com o nome da receita.

Podemos criar nossa própria anotação BeerLang e registrá-la como parte da DSL BeerLang. Agora estamos tentando executar esse código, e ele não deve ser compilado conosco.



Agora nos dizem que, em princípio, o nome não pode ser usado nesse contexto. Para isso, o DSLMarker é necessário porque o compilador dentro do construtor não nos permitiu usar o campo externo, se tivermos o mesmo dentro dele para que não haja conflito de nomenclatura. O código é fixo assim, e obtemos nossa receita.



Pazzler número 7



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

Este quebra-cabeças é um dos funcionários da JetBrains. Kotlin tem um recurso quando. É para todas as ocasiões, permite que você escreva códigos legais, geralmente é usado junto com classes seladas para o design da API.

Nesse caso, temos uma função f () que pega um booleano e imprime algo dependendo de verdadeiro e falso.

O que vai acontecer?

a) verdadeiro VERDADEIRO; falso falso
b) verdadeiro VERDADEIRO; false VERDADEIRO
c) verdadeiro FALSO; falso falso
d) Nenhuma das opções acima

Vamos ver


Porque Primeiro, calculamos a expressão x == true: por exemplo, no primeiro caso, será true == true, o que significa true. E também há uma comparação com o padrão que passamos quando.

E quando x é definido como false, a avaliação de x == true nos fornecerá false, no entanto, a amostra também será falsa - portanto, o exemplo corresponderá à amostra.

Há duas maneiras de corrigir esse código: uma é remover “x ==” nos dois casos:

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

A segunda opção é remover (x) depois de quando. Quando funciona com quaisquer condições e, em seguida, não corresponde à amostra.

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


Número Pazzler 8


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

O Kotlin foi comercializado como uma linguagem "null safe". Imagine que temos uma classe abstrata, ela tem algum nome e também uma propriedade que retorna o logotipo dessa linguagem: a primeira letra do nome, por via das maiúsculas, em maiúscula (de repente foi esquecido fazer o capital inicial).

Como o idioma é nulo seguro, alteraremos o nome e provavelmente obteremos o logotipo correto, que é uma letra. O que realmente recebemos?

a) K
b) NullPointerException
c) IllegalStateException
d) Não compilado

Lançamento!


Temos uma NullPointerException, que não devemos receber. O problema é que o construtor da superclasse é chamado primeiro, o código tenta inicializar o logotipo da propriedade e recebe o nome char de zero e, nesse momento, o nome é nulo, portanto ocorre uma NullPointerException.

A melhor maneira de corrigir isso é fazer o seguinte:

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

Se rodarmos esse código, obteremos "K". Agora a classe base chamará o construtor da classe base, na verdade chamará o nome do getter e obterá o Kotlin.

A propriedade é um ótimo recurso no Kotlin, mas você precisa ter muito cuidado ao substituir as propriedades, porque é muito fácil esquecer, cometer um erro ou garantir a coisa errada.


Número Pazzler 9


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

Existe uma lista mutável de algumas coisas assustadoras. Se ele lembra Scala, não é em vão, porque realmente se parece. Existe uma lista lambd, tomamos dois contadores - I e j, incrementamos e depois fazemos algo com eles. O que vai acontecer?

a) 1 1; 2 2; 3 3
b) 1 3; 2 3; 3 3
c) 3 1; 3 2; 3 3
d) nenhuma das opções acima

Vamos correr


Temos 3 1; 3 2; 3 3. Isso acontece porque i é uma variável e ele manterá seu valor até o final da função. E j é passado por valor.

Se, em vez de var i = 0, houvesse val i = 0, isso não funcionaria, mas não poderíamos incrementar a variável.

Aqui no Kotlin usamos o fechamento, esse recurso não está em Java. É muito legal, mas pode nos morder se não usarmos imediatamente o valor de i, mas passá-lo para o lambda, que inicia mais tarde e vê o último valor dessa variável. E j é passado por valor, porque as variáveis ​​na condição do loop - são iguais a val, não mudam mais seu valor.

Em JavaScript, a resposta seria "3 3; 3 3; 3 3 ”, porque nada é transmitido por valor.


Pazzler número 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) 

Temos uma função foo (), pega dois booleanos, os imprime, tudo parece simples. E temos muitos números, resta ver qual é o número maior que o outro e decidir qual opção está correta.

a) verdadeiro, verdadeiro
b) falso, falso
c) nulo, nulo
d) não compilado

Lançamos


Não compilado.

O problema é que o compilador pensa que isso é semelhante aos parâmetros genéricos: com <a, b>. Embora pareça que "c" não seja uma classe, não está claro por que ele deve ter parâmetros genéricos.

Se o código fosse assim, funcionaria bem:

 foo(c > a, b > d) 

Parece-me que isso é um bug no compilador. Mas quando eu vou até Andrei Breslav com qualquer quebra-cabeças, ele diz "isso ocorre porque o analisador é assim, eles não queriam que fosse muito lento". Em geral, ele sempre encontra uma explicação do porquê.

Infelizmente é assim. Ele disse que eles não vão consertar, porque o analisador
Kotlin ainda não conhece a semântica. A análise ocorre primeiro e depois a transmite para outro componente do compilador. Infelizmente, isso provavelmente continuará assim. Portanto, não escreva dois colchetes angulares e nenhum código no meio!

Pazzler número 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″) 

O delegado é um ótimo recurso no Kotlin. A propósito, Andrei Breslav diz que esse é um recurso que ele removeria de bom grado do idioma. Ele não gosta mais. Agora, talvez, vamos descobrir o porquê! E ele também disse que os objetos complementares são feios.

Mas as classes de dados são definitivamente lindas. Temos uma classe de dados Container, que leva um nome e itens para si. Ao mesmo tempo, no Contêiner, implementamos o tipo de itens, isto é Lista, e delegamos todos os seus métodos aos itens.

Então usamos outro recurso interessante - desestruturar. Nós "destruímos" os elementos de nome e itens do container e os exibimos na tela. Tudo parece ser simples e claro. O que vai acontecer?

a) Olá Kotlin, [1, 2, 3]
b) Olá Kotlin, 1
c) Olá 1, 2
d) Olá Kotlin, 2

Lançamos


A opção mais obscura é d. Ele acaba sendo verdade. Como se viu, os itens simplesmente desaparecem da coleção de itens, e não do começo ou do fim, mas apenas no meio. Porque

O problema com a desestruturação é que, devido à delegação, todas as coleções no Kotlin também são
têm sua própria opção de desestruturação. Eu posso escrever val (I, j) = listOf (1, 2) e obter esses 1 e 2 em variáveis, ou seja, List implementou as funções component1 () e
component2 ().

A classe de dados também possui component1 () e component2 (). Mas como o segundo componente, neste caso, é privado, o público da List vence, então o segundo elemento é retirado da List e chegamos aqui 2. A moral é muito simples: não faça isso, não faça isso.

Número Pazzler 12


O próximo quebra-cabeças é muito assustador. Esta é uma pessoa submissa que de alguma forma está conectada com Kotlin, para que ele saiba o que está escrevendo.

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

Temos uma função de extensão em Any anulável, ou seja, ela pode ser aplicada a qualquer coisa. Este é um recurso muito útil. Se ainda não estiver no seu projeto, vale a pena adicionar, pois pode colocar tudo o que você deseja em qualquer coisa. Então pegamos 42 e lançamos no Nada.

Bem, se queremos ter certeza de que fizemos algo importante, podemos fazê-lo !!! write !!!!, o compilador Kotlin permite que você faça isso: se estiver faltando dois pontos de exclamação, escreva pelo menos vinte e seis.

Então nós fazemos se (verdadeiro), e então eu mesmo não entendo nada ... Vamos escolher imediatamente o que acontece.

a) 87
b) Kotlin.Unit
c) ClassCastException
d) Não compilado

Assistindo


É muito difícil dar uma explicação lógica. Muito provavelmente, a Unidade aqui se deve ao fato de que não há mais nada a ser empurrado para lá. Este é um código inválido, mas funciona porque usamos o Nothing. Fizemos upload de algo para Nothing, e esse é um tipo especial que informa ao compilador que uma instância desse tipo nunca deve aparecer. O compilador sabe que, se houver a possibilidade do aparecimento de Nothing, o que é impossível por definição, você não poderá verificar mais, essa é uma situação impossível.

Provavelmente, este é um bug no compilador, a equipe do JetBrains chegou a dizer que talvez esse bug seja corrigido algum dia, isso não é uma prioridade. O truque é que enganamos o compilador aqui por causa desse elenco. Se você remover a linha 42.asGeneric <Nothing> () !!! e parar de trapacear, o código irá parar de compilar. E se formos embora, o compilador enlouquece, acha que essa é uma expressão impossível e enfia o que quer que esteja lá.

Eu entendo isso Talvez alguém um dia o explique melhor.


Pazzler número 13


Temos uma característica muito interessante. Você pode usar a injeção de dependência, ou pode ficar sem ela, criar singletones através do objeto e executar o programa legal. Por que você precisa de Koin, Dagger ou algo assim? Testar, no entanto, será difícil.

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

Temos a classe A aberta para herança, é preciso algo dentro de si, criamos dois objetos'a, singleton, B e C, ambos são herdados de A e passam um ao outro por lá. Ou seja, um excelente ciclo é formado. Em seguida, imprimimos o que B e C receberam.

a) nulo; nulo
b) C; nulo
c) ExceptionInInitializerError
d) Não compilado

Lançamos


A opção correta é C; nulo

Alguém poderia pensar que, quando o primeiro objeto é inicializado, o segundo ainda não está lá. Mas, quando deduzimos isso, C não tem B. Ou seja, a ordem inversa é obtida: por algum motivo, o compilador decidiu inicializar C primeiro e depois inicializou B junto com C. Parece ilógico, seria lógico, pelo contrário, nulo ; B.

Mas o compilador tentou fazer alguma coisa, não teve sucesso, ele deixou nulo lá e decidiu não jogar nada para nós. Também poderia ser assim.

Se houver? no tipo de parâmetro, remova?, então não funcionará.



Podemos dizer bem ao compilador que, quando o nulo foi resolvido, ele tentou, mas falhou, mas o quê? não, ele nos lança uma exceção de que é impossível fazer um ciclo.

Pazzler №14


A versão 1.3 lançou grandes novas corotinas no Kotlin. Pensei por um longo tempo como criar um quebra-cabeças sobre corutin, para que alguém pudesse entender. Para algumas pessoas, qualquer código com corotinas é um quebra-cabeças.

Na versão 1.3, foram alterados alguns nomes de funções que estavam na versão 1.2 da API experimental. Por exemplo, buildSequence () é renomeado para simplesmente sequence (). Ou seja, podemos criar excelentes seqüências com a função yield, loops infinitos e, em seguida, podemos tentar obter algo dessa sequência.

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

Eles disseram com corotinas que todas as primitivas interessantes que estão em outros idiomas, como yield, podem ser feitas como funções de biblioteca, porque yield é uma função de suspensão que pode ser interrompida.

O que vai acontecer?

a) [1, 2, 3]
b) [0, 1, 2]
c) Loop infinito
d) Nenhuma das opções acima

Lançamento!


A opção correta é a última.

A sequência é uma engenhoca preguiçosa e, quando nos apegamos a ela, também é preguiçosa. Mas se você adicionar à lista, ela realmente será impressa [0, 1, 2].

A resposta correta não está relacionada às corotinas. As corotinas realmente funcionam, são fáceis de usar. Para a função de sequência e rendimento, você nem precisa conectar uma biblioteca a corotinas, tudo já está na biblioteca padrão.

Pazzler №15


Este quebra-cabeças também é controlado pelo desenvolvedor do JetBrains. Existe um código tão infernal:

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

Quando o vi pela primeira vez, durante o KotlinConf, não consegui dormir, tentei entender o que era. Esse código enigmático pode ser escrito em Kotlin; portanto, se alguém pensou que Scalaz era assustador, então em Kotlin também é possível.

Vamos adivinhar:

a) Kotlin.Unit
b) Kotlin.Nada
c) Não compilado
d) Nenhuma das opções acima

Vamos correr


Temos uma unidade que veio do nada.

Porque Primeiro, atribuímos a variável lambda: {->} - este é um código válido, você pode escrever um lambda vazio. Não possui parâmetros, não retorna nada. Consequentemente, ele retorna Unit.

Atribuímos um lambda à variável e gravamos imediatamente a extensão nesse lambda e, em seguida, executamos. De fato, ele simplesmente reserva o Kotlin.Unit.

Nesse lambda, você pode escrever uma função de extensão:

 .fun Function<*>.(){} 

É declarado no tipo Função <*>, e o que temos no topo também é adequado para ele. Na verdade, é a função <Unit>, mas eu não escrevi Unit que não estava claro. Você sabe como funciona um asterisco no Kotlin? , Java. , .

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

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

: , — 6 .

Source: https://habr.com/ru/post/pt440974/


All Articles