
¿Puedes predecir cómo se comportará ese código de Kotlin? ¿Compilará lo que saldrá y por qué?
No importa cuán bueno sea el lenguaje de programación, puede arrojarse de manera tal que solo se rasque la parte posterior de la cabeza. Kotlin no es una excepción: también contiene acertijos cuando incluso un código muy corto tiene un comportamiento inesperado.
En 2017, publicamos en Habré una
selección de tales rompecabezas de
Anton Keks antonkeks . Y luego actuó con nosotros en Mobius con la segunda selección, y ahora también lo tradujimos para Habr en una vista de texto, ocultando las respuestas correctas debajo de los spoilers.
También adjuntamos la grabación de video del discurso, si algo parece incomprensible en el texto, también puede contactarla.
La primera mitad de los rompecabezas están dirigidos a aquellos que no están muy familiarizados con Kotlin; la segunda mitad es para desarrolladores hardcore de Kotlin. Comenzaremos todo en Kotlin 1.3, incluso con el modo progresivo habilitado. Los códigos fuente de Puzzler están
en GitHub . Quien tenga nuevas ideas, envíe solicitudes de extracción.
Pazzler numero 1
fun hello(): Boolean { println(print(″Hello″) == print(″World″) == return false) } hello()
Ante nosotros hay una simple función de saludo, ejecuta varias impresiones. Y lanzamos esta función en sí. Una simple pregunta de overclocking: ¿qué debería imprimir?
a) HelloWorld
b) HelloWorldfalse
c) HelloWorldtrue
d) No compilado
Respuesta correcta
La primera opción era correcta. La comparación se activa después de que ambas impresiones ya hayan comenzado, no puede comenzar antes. ¿Por qué se compila ese código? Cualquier función distinta de devolver Nada devuelve algo. Como todo en Kotlin es una expresión, incluso return también es una expresión. El tipo de retorno de return es Nothing, se convierte a cualquier tipo, por lo que puede comparar de esta manera. E print devuelve Unit, por lo que Unit se puede comparar con Nothing varias veces, y todo funciona muy bien.
Pazzler número 2
fun printInt(n: Int) { println(n) } printInt(-2_147_483_648.inc())
Sugerencia que no adivina: un número aterrador es realmente el menor entero con signo de 32 bits posible.
Todo parece simple aquí. Kotlin tiene excelentes funciones de extensión como .inc () para incrementar. Podemos llamarlo en Int, y podemos imprimir el resultado. Que va a pasar
a) -2147483647
b) -2147483649
c) 2147483647
d) Ninguna de las anteriores
Lanzamiento
Como puede ver en el mensaje de error, aquí está el problema con Long. ¿Pero por qué tanto?
Las funciones de extensión tienen prioridad, y el compilador primero ejecuta inc (), y luego el operador menos. Si se elimina inc (), será Int y todo funcionará. Pero inc (), comenzando primero, convierte 2_147_483_648 en Long, porque este número sin menos ya no es válido Int. Resulta largo, y solo entonces se llama menos. Todo esto ya no se puede pasar a la función printInt (), ya que requiere un Int.
Si cambiamos la llamada printInt a una impresión normal, que puede aceptar Long, entonces la segunda opción será correcta.

Vemos que esto es realmente largo. Cuidado con esto: no todas las piezas del rompecabezas se pueden encontrar en código real, pero este sí.
Pazzler número 3
var x: UInt = 0u println(x--.toInt()) println(--x)
En Kotlin 1.3 llegaron nuevas características excelentes. Además de la versión final de corutina, nosotros
ahora finalmente tienen números sin signo. Esto es necesario, especialmente si está escribiendo algún tipo de código de red.
Ahora, para literales, incluso hay una letra especial u, podemos definir constantes, podemos, como en el ejemplo, decrementar x y convertir a Int. Les recuerdo que Int está familiarizado con nosotros.
Que va a pasar
a) -1 4294967294
b) 0 4294967294
c) 0 -2
d) No compilado
4294967294 es el número máximo de 32 bits que se puede obtener.
Lanzamiento
Opción correcta b.
Aquí, como en la versión anterior: primero, toInt () se invoca en x, y solo luego disminuye. Se muestra el resultado del decremento unsigned, y este es el máximo de unsignedInt.
Lo más interesante es que si escribe así, el código no se compilará:
println(x--.toInt()) println(--x.toInt())
Y para mí es muy extraño que la primera línea funcione y la segunda, no, esto es ilógico.
Y en la versión preliminar, la opción correcta sería C, tan bien hecha en JetBrains que corrige los errores antes del lanzamiento de la versión final.
Pazzler número 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)
Nos encontramos con este caso en código real. Nosotros en Codeborne hicimos Coding Dojo, lo implementamos juntos en Kotlin
Game of Life . Como puede ver, no es muy conveniente trabajar con matrices de niveles múltiples en Kotlin.
En Game of Life, una parte importante del algoritmo es determinar el número de vecinos para una celda. Todos los pequeños alrededor son vecinos, y depende de si la célula vive o muere. En este código, puede contarlos y asumir lo que sucede.
a) 6
b) 3
c) 2
d) No compilado
Vamos a ver
La respuesta correcta es 3.
El hecho es que la ventaja de la primera línea se mueve hacia abajo, y Kotlin piensa que esto es unaryPlus (). Como resultado, solo se suman las tres primeras celdas. Si queremos escribir este código en varias líneas, necesitamos mover el más hacia arriba.
Este es otro de los "malos acertijos". Recuerde, en Kotlin no necesita transferir la declaración a una nueva línea, de lo contrario, puede considerarla unaria.

No he visto situaciones en las que se necesita unaryPlus en código real, excepto DSL. Este es un tema muy extraño.
Este es el precio que pagamos por la ausencia de punto y coma. Si lo fueran, estaría claro cuando una expresión termina y comienza otra. Y sin ellos, el compilador debe tomar la decisión. Los avances de línea para el compilador a menudo significan que tiene sentido tratar de examinar las líneas por separado.
Pero hay un lenguaje JavaScript muy bueno en el que tampoco puedes escribir punto y coma, y este código seguirá funcionando correctamente.
Pazzler número 5
val x: Int? = 2 val y: Int = 3 val sum = x?:0 + y println(sum)
Este rompecabezas es presentado por el orador de KotlinConf Thomas Nild.
Kotlin tiene una gran característica de tipos anulables. Tenemos x anulable, y podemos convertirlo, si resulta ser nulo, a través del operador Elvis a algún valor normal.
Que va a pasar
a) 3
b) 5
c) 2
d) 0
Lanzamiento
El problema está nuevamente en el orden o prioridad de los operadores. Si reformateamos esto, entonces el formato oficial lo hará:
val sum = x ?: 0+y
El formato ya sugiere que 0 + y comienza primero, y solo entonces x?:. Por lo tanto, por supuesto, queda 2, porque X es dos, no es nulo.
Pazzler número 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 } }
Cuando me llamaron aquí, me prometieron elaborar cerveza. Voy a buscarlo esta noche, aún no lo he visto. Kotlin tiene un gran tema: los constructores. Con cuatro líneas de código, escribimos nuestro DSL y luego lo creamos a través de los constructores.
En primer lugar, creamos IPA, agregamos lúpulos llamados Cascade, 100 gramos en el minuto 15 de cocción, y luego imprimimos esta receta. Que hicimos
a) Receta (nombre = IPA simple, lúpulo = [Lúpulo (nombre = Cascada, al minuto = 15, gramos = 100)])
b) IllegalArgumentException
c) No compilado
d) Ninguna de las anteriores
Lanzamiento
Tenemos algo similar a la cerveza artesanal, pero no hay lúpulo, desapareció. Querían una IPA, pero obtuvieron Baltic 7.
Aquí es donde ocurrió el choque de nombres. El campo en Hops en realidad se llama kind, y en la línea name = ″ Cascade ″ usamos name, que se almacena en caché con el nombre de la receta.
Podemos crear nuestra propia anotación BeerLang y registrarla como parte del DSL BeerLang. Ahora estamos tratando de ejecutar este código, y no debería compilarse con nosotros.

Ahora se nos dice que, en principio, el nombre no puede usarse desde este contexto. Para esto, se necesita DSLMarker porque el compilador dentro del constructor no nos permitió usar el campo externo, si tenemos el mismo dentro de él para que no haya conflicto de nombres. El código se arregla así, y obtenemos nuestra receta.

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 rompecabezas es uno de los empleados de JetBrains. Kotlin tiene una función cuándo. Es para todas las ocasiones, le permite escribir código genial, a menudo se usa junto con clases selladas para el diseño de API.
En este caso, tenemos una función f () que toma un booleano e imprime algo dependiendo de verdadero y falso.
Que va a pasar
a) verdadero VERDADERO; falso falso
b) verdadero VERDADERO; falso VERDADERO
c) verdadero FALSO; falso falso
d) Ninguna de las anteriores
Vamos a ver
Por qué Primero, calculamos la expresión x == verdadero: por ejemplo, en el primer caso, será verdadero == verdadero, lo que significa verdadero. Y luego también hay una comparación con el patrón que pasamos cuando.
Y cuando x se establece en falso, evaluar x == verdadero nos dará falso, sin embargo, la muestra también será falsa, por lo que el ejemplo coincidirá con la muestra.
Hay dos formas de arreglar este código, una es eliminar "x ==" en ambos casos:
fun f(x: Boolean) { when (x) { true -> println(″$x TRUE″) false -> println(″$x FALSE″) } } f(true) f(false)
La segunda opción es eliminar (x) después de cuándo. Cuando funciona con cualquier condición, y luego no coincidirá con la muestra.
fun f(x: Boolean) { when { x == true -> println(″$x TRUE″) x == false -> println(″$x FALSE″) } } f(true) f(false)
Pazzler número 8
abstract class NullSafeLang { abstract val name: String val logo = name[0].toUpperCase() } class Kotlin : NullSafeLang() { override val name = ″Kotlin″ } print(Kotlin().logo)
Kotlin fue comercializado como un lenguaje "nulo seguro". Imagine que tenemos una clase abstracta, tiene un nombre, así como una propiedad que devuelve el logotipo de este idioma: la primera letra del nombre, por si acaso, en mayúscula (de repente se olvidó de hacer la capital inicial).
Dado que el idioma es nulo seguro, cambiaremos el nombre y probablemente deberíamos obtener el logotipo correcto, que es una letra. ¿Qué es lo que realmente obtenemos?
a) K
b) NullPointerException
c) IllegalStateException
d) No compilado
Lanzamiento
Tenemos una NullPointerException, que no deberíamos recibir. El problema es que primero se llama al constructor de la superclase, el código intenta inicializar el logotipo de la propiedad y tomar el nombre char de cero, y en este punto el nombre es nulo, por lo que se produce una NullPointerException.
La mejor manera de solucionar esto es hacer esto:
class Kotlin : NullSafeLang() { override val name get() = ″Kotlin″ }
Si ejecutamos dicho código, obtenemos "K". Ahora la clase base llamará al constructor de la clase base, en realidad llamará al nombre getter y obtendrá Kotlin.
La propiedad es una gran característica en Kotlin, pero debe ser muy cuidadoso cuando anula las propiedades, porque es muy fácil de olvidar, cometer un error o asegurar algo incorrecto.
Pazzler número 9
val result = mutableListOf<() -> Unit>() var i = 0 for (j in 1..3) { i++ result += { print(″$i, $j; ″) } } result.forEach { it() }
Hay una lista mutable de algunas cosas aterradoras. Si te recuerda a Scala, entonces no es en vano, porque realmente parece. Hay una lista lambd, tomamos dos contadores: I y j, incrementamos y luego hacemos algo con ellos. Que va a pasar
a) 1 1; 2 2; 3 3
b) 1 3; 2 3; 3 3
c) 3 1; 3 2; 3 3
d) ninguno de los anteriores
Vamos a correr
Tenemos 3 1; 3 2; 3 3. Esto sucede porque i es una variable y conservará su valor hasta el final de la función. Y j se pasa por valor.
Si en lugar de var i = 0 hubiera val i = 0, esto no funcionaría, pero no podríamos incrementar la variable.
Aquí en Kotlin usamos el cierre, esta característica no está en Java. Es genial, pero puede mordernos si no usamos inmediatamente el valor de i, sino que lo pasamos a la lambda, que comienza más tarde y ve el último valor de esta variable. Y j se pasa por valor, porque las variables en la condición de bucle: son las mismas que val, ya no cambian su valor.
En JavaScript, la respuesta sería "3 3; 3 3; 3 3 ”, porque no hay 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)
Tenemos una función foo (), toma dos booleanos, los imprime, todo parece ser simple. Y tenemos un montón de números, queda por ver qué figura es más grande que la otra, y decidir qué opción es la correcta.
a) cierto, cierto
b) falso, falso
c) nulo, nulo
d) no compilado
Lanzamos
No compilado
El problema es que el compilador piensa que esto es similar a los parámetros genéricos: con <a, b>. Aunque parece que "c" no es una clase, no está claro por qué debería tener parámetros genéricos.
Si el código fuera así, funcionaría bien:
foo(c > a, b > d)
Me parece que este es un error en el compilador. Pero cuando me acerco a Andrei Breslav con tal enigma, dice: "Esto se debe a que el analizador es así, no querían que fuera demasiado lento". En general, siempre encuentra una explicación de por qué.
Lamentablemente, esto es así. Dijo que no lo arreglarán, porque el analizador en
Kotlin aún no conoce la semántica. El análisis ocurre primero y luego lo pasa a otro componente del compilador. Desafortunadamente, esto probablemente seguirá siendo así. ¡Así que no escriba dos paréntesis angulares y ningún código en el medio!
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″)
Delegar es una gran característica en Kotlin. Por cierto, Andrei Breslav dice que esta es una característica que con mucho gusto eliminaría del idioma, ya no le gusta. Ahora, tal vez, descubriremos por qué. Y también dijo que los objetos de compañía son feos.
Pero las clases de datos son definitivamente hermosas. Tenemos un contenedor de clase de datos, toma un nombre y elementos para sí mismo. Al mismo tiempo, en el Contenedor, implementamos el tipo de elementos, esto es Lista, y delegamos todos sus métodos a los elementos.
Luego usamos otra característica interesante: la desestructuración. "Destruimos" los elementos de nombre y elementos del Contenedor y los mostramos en la pantalla. Todo parece ser simple y claro. Que va a pasar
a) Hola Kotlin, [1, 2, 3]
b) Hola Kotlin, 1
c) Hola 1, 2
d) Hola Kotlin, 2
Lanzamos
La opción más oscura es d. Resulta ser cierto. Al final resultó que, los elementos simplemente desaparecen de la colección de elementos, y no desde el principio o desde el final, sino solo en el medio. Por qué
El problema con la desestructuración es que debido a la delegación, todas las colecciones en Kotlin también
tienen su propia opción de desestructuración. Puedo escribir val (I, j) = listOf (1, 2), y obtener estos 1 y 2 en variables, es decir, List ha implementado las funciones componente1 () y
componente2 ().
La clase de datos también tiene componente1 () y componente2 (). Pero dado que el segundo componente en este caso es privado, el que es público en List gana, por lo que el segundo elemento se toma de List, y llegamos aquí 2. La moraleja es muy simple: no hagas eso, no hagas eso.
Pazzler número 12
El siguiente rompecabezas es muy aterrador. Esta es una persona sumisa que de alguna manera está conectada con Kotlin, por lo que sabe lo que está escribiendo.
fun <T> Any?.asGeneric() = this as? T 42.asGeneric<Nothing>()!!!! val a = if (true) 87 println(a)
Tenemos una función de extensión en Anulable Any, es decir, se puede aplicar a cualquier cosa. Esta es una característica muy útil. Si aún no está en su proyecto, vale la pena agregarlo, porque puede poner todo lo que desee en cualquier cosa. Luego tomamos 42 y lo lanzamos a la nada.
Bueno, si queremos estar seguros de que hemos hecho algo importante, ¡podemos hacerlo! escriba !!!!, el compilador de Kotlin le permite hacer esto: si le faltan dos signos de exclamación, escriba al menos veintiséis.
Luego hacemos if (verdadero), y luego yo mismo no entiendo nada ... Elegimos inmediatamente lo que sucede.
a) 87
b) Kotlin.Unit
c) ClassCastException
d) No compilado
Viendo
Es muy difícil dar una explicación lógica. Lo más probable es que la Unidad aquí se deba al hecho de que no hay nada más que empujar allí. Este es un código no válido, pero funciona porque usamos Nothing. Hemos subido algo a Nothing, y este es un tipo especial que le dice al compilador que nunca debería aparecer una instancia de este tipo. El compilador sabe que si existe la posibilidad de la aparición de Nothing, lo cual es imposible por definición, entonces no puede verificar más, esta es una situación imposible.
Lo más probable es que este sea un error en el compilador, el equipo de JetBrains incluso dijo que tal vez este error se solucionará algún día, esto no es una prioridad. El truco es que engañamos al compilador aquí debido a este elenco. Si elimina la línea 42.asGeneric <Nthing> () !!! y deje de hacer trampa, el código dejará de compilarse. Y si nos vamos, el compilador se vuelve loco, piensa que esta es una expresión imposible, y mete lo que sea que esté allí.
Entiendo eso Quizás alguien algún día lo explique mejor.
Pazzler número 13
Tenemos una característica muy interesante. Puede usar la inyección de dependencia, o puede prescindir de ella, hacer singletones a través del objeto y ejecutar su programa genial. ¿Por qué necesitas Koin, Dagger o algo así? Las pruebas, sin embargo, serán difíciles.
open class A(val x: Any?) { override fun toString() = javaClass.simpleName } object B : A(C) object C : A(B) println(Bx) println(Cx)
Tenemos la clase A abierta para la herencia, se necesita algo dentro de sí misma, creamos dos objetos: a, singleton, B y C, ambos se heredan de A y se cruzan allí. Es decir, se forma un excelente ciclo. Luego imprimimos lo que obtuvieron B y C.
a) nulo; nulo
b) C; nulo
c) ExceptionInInitializerError
d) No compilado
Lanzamos
La opción correcta es C; nulo
Uno podría pensar que cuando se inicializa el primer objeto, el segundo todavía no está allí. Pero, cuando deducimos esto, C carece de B. Es decir, se obtiene el orden inverso: por alguna razón, el compilador decidió inicializar C primero, y luego inicializó B junto con C. Parece ilógico, sería lógico, por el contrario, nulo ; B.
Pero el compilador intentó hacer algo, no tuvo éxito, dejó el nulo allí y decidió no tirarnos nada. Podría ser así también.
Si alguno? en el tipo de parámetro, ¿eliminar ?, entonces no funcionará.

Podemos decirle bien al compilador que cuando se resolvió nulo, lo intentó, pero falló, pero ¿qué? no, nos lanza una excepción de que es imposible hacer un ciclo.
Pazzler №14
La versión 1.3 lanzó grandes nuevas rutinas en Kotlin. Durante mucho tiempo pensé en cómo encontrar un enigma sobre la corutina, para que alguien pudiera entenderlo. Creo que para algunas personas cualquier código con corutinas es un enigma.
En 1.3, algunos nombres de funciones cambiaron que estaban en 1.2 en la API experimental. Por ejemplo, buildSequence () se renombra para simplemente secuenciar (). Es decir, podemos hacer secuencias excelentes con la función de rendimiento, bucles infinitos, y luego podemos intentar sacar algo de esta secuencia.
package coroutines.yieldNoOne val x = sequence { var n = 0 while (true) yield(n++) } println(x.take(3))
Dijeron con corutinas que todas las primitivas geniales que están en otros idiomas, como el rendimiento, se pueden hacer como funciones de biblioteca, porque el rendimiento es una función de suspensión que se puede interrumpir.
Que va a pasar
a) [1, 2, 3]
b) [0, 1, 2]
c) bucle infinito
d) Ninguna de las anteriores
Lanzamiento
La opción correcta es la última.
La secuencia es un artilugio vago, y cuando nos aferramos a él, también es vago. Pero si agrega toList, realmente imprimirá [0, 1, 2].
La respuesta correcta no está relacionada con las rutinas en absoluto. Las corutinas realmente funcionan, son fáciles de usar. Para la función de secuencia y rendimiento, ni siquiera necesita conectar una biblioteca con corutinas, todo ya está en la biblioteca estándar.
Pazzler №15
Este rompecabezas también está sometido por el desarrollador de JetBrains. Hay un código tan infernal:
val whatAmI = {->}.fun Function<*>.(){}() println(whatAmI)
Cuando lo vi por primera vez, durante KotlinConf, no pude dormir, intenté entender de qué se trataba. Tal código críptico se puede escribir en Kotlin, por lo que si alguien pensó que Scalaz daba miedo, entonces en Kotlin también es posible.
Supongamos:
a) Kotlin.Unit
b) Kotlin.Nada
c) No compilado
d) Ninguna de las anteriores
Vamos a correr
Tenemos una Unidad que vino de la nada.
Por qué Primero asignamos la variable lambda: {->} - este es un código válido, puede escribir un lambda vacío. No tiene parámetros, no devuelve nada. En consecuencia, devuelve Unidad.
Asignamos un lambda a la variable e inmediatamente escribimos la extensión a este lambda, y luego lo ejecutamos. De hecho, simplemente reservará Kotlin.Unit.
Luego, en esta lambda, puede escribir una función de extensión:
.fun Function<*>.(){}
Se declara en el tipo Función <*>, y lo que tenemos encima también es adecuado para ello. En realidad, es la Función <Unidad>, pero no escribí Unidad que no estaba claro. ¿Sabes cómo funciona un asterisco en Kotlin? , Java. , .
, Unit {}, , void-. , , . -, — .
. , Kotlin — . iOS- , , Kotlin !
Mobius, : Mobius 22-23 . Kotlin — «Coroutining Android Apps» . ( Android, iOS), — , 1 .
: , — 6 .