Casse-tête Kotlin, Vol. 2: un nouveau lot de puzzles



Pouvez-vous prédire comment un tel code Kotlin se comportera? Compilera-t-il ce qui sortira et pourquoi?

Peu importe la qualité du langage de programmation, il peut lancer de telle sorte qu'il ne reste plus qu'à vous gratter la tête. Kotlin ne fait pas exception - il contient également des puzzles, même si un morceau de code très court a un comportement inattendu.

En 2017, nous avons publié sur Habré une sélection de tels puzzles d' Anton Keks antonkeks . Et plus tard, il a joué avec nous sur Mobius avec la deuxième sélection, et nous l'avons maintenant également traduit pour Habr en une vue texte, cachant les bonnes réponses sous les spoilers.

Nous joignons également l'enregistrement vidéo du discours, si quelque chose semble incompréhensible dans le texte, vous pouvez également la contacter.


La première moitié des puzzles s'adresse à ceux qui ne sont pas très familiers avec Kotlin; la seconde moitié est pour les développeurs hardcore de Kotlin. Nous allons tout démarrer sur Kotlin 1.3, même avec le mode progressif activé. Les codes sources de Puzzler sont sur GitHub . Quiconque propose de nouvelles idées, envoie des demandes de tirage.

Pazzler numéro 1


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

Avant nous est une simple fonction bonjour, elle exécute plusieurs impressions. Et nous lançons cette fonction elle-même. Une simple question d'overclocking: que faut-il imprimer?

a) HelloWorld
b) HelloWorldfalse
c) HelloWorldtrue
d) Non compilé

Bonne réponse


La première option était correcte. La comparaison est déclenchée une fois que les deux impressions ont déjà commencé, elle ne peut pas commencer plus tôt. Pourquoi ce code est-il compilé? Toute fonction autre que le retour de Nothing renvoie quelque chose. Puisque tout dans Kotlin est une expression, même le retour est aussi une expression. Le type de retour de retour est Nothing, il est converti en n'importe quel type, vous pouvez donc comparer comme ceci. Et l'impression renvoie Unit, de sorte que Unit peut être comparé à Nothing un certain nombre de fois, et tout fonctionne très bien.

Pazzler numéro 2


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

Astuce que vous ne devinez pas: un nombre effrayant est vraiment le plus petit entier signé 32 bits possible.

Ici, tout semble simple. Kotlin a de grandes fonctions d'extension comme .inc () pour l'incrémentation. Nous pouvons l'appeler sur Int, et nous pouvons imprimer le résultat. Que va-t-il se passer?

a) -2147483647
b) -2147483649
c) 2147483647
d) Aucune de ces réponses

Lancez!


Comme vous pouvez le voir dans le message d'erreur, voici le problème avec Long. Mais pourquoi longtemps?

Les fonctions d'extension ont la priorité, et le compilateur exécute d'abord inc (), puis l'opérateur moins. Si inc () est supprimé, ce sera Int et tout fonctionnera. Mais inc (), commençant en premier, transforme 2_147_483_648 en Long, car ce nombre sans signe négatif n'est plus valide Int. Il s'avère Long, et alors seulement le moins est appelé. Tout cela ne peut plus être transmis à la fonction printInt (), car elle nécessite un Int.

Si nous changeons l'appel printInt en une impression régulière, qui peut accepter Long, la deuxième option sera correcte.



Nous voyons que c'est en fait long. Méfiez-vous de cela: toutes les pièces du puzzle ne peuvent pas être exécutées dans du vrai code, mais celle-ci le peut.

Pazzler numéro 3


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

Dans Kotlin 1.3 est venu de nouvelles fonctionnalités intéressantes. En plus de la version finale de la corutine, nous
ont maintenant enfin des numéros non signés. Cela est nécessaire, surtout si vous écrivez une sorte de code réseau.

Maintenant, pour les littéraux, il y a même une lettre spéciale u, nous pouvons définir des constantes, nous pouvons, comme dans l'exemple, décrémenter x et convertir en Int. Je vous rappelle qu'Int nous connaît.

Que va-t-il se passer?

a) -1 4294967294
b) 0 4294967294
c) 0 -2
d) Non compilé

4294967294 est le nombre maximal de 32 bits qui peut être obtenu.

Lancez!


Bonne option b.

Ici, comme dans la version précédente: d'abord, toInt () est appelé sur x, et ensuite seulement décrémente. Le résultat de la décrémentation non signé s'affiche, et c'est le maximum de unsignedInt.

La chose la plus intéressante est que si vous écrivez comme ça, le code ne compilera pas:

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

Et pour moi, il est très étrange que la première ligne fonctionne, et la seconde - non, c'est illogique.

Et dans la version préliminaire, l'option correcte serait C, si bien fait dans JetBrains qui corrige les bugs avant la sortie de la version finale.

Pazzler numéro 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) 

Nous avons rencontré ce cas en code réel. Chez Codeborne, nous avons fait Coding Dojo, nous l'avons implémenté ensemble sur le Kotlin Game of Life . Comme vous pouvez le voir, il n'est pas très pratique de travailler avec des tableaux à plusieurs niveaux sur Kotlin.

Dans Game of Life, une partie importante de l'algorithme consiste à déterminer le nombre de voisins pour une cellule. Tous les petits autour sont des voisins, et cela dépend si la cellule vit ou meurt. Dans ce code, vous pouvez compter celles et supposer ce qui se passe.

a) 6
b) 3
c) 2
d) Non compilé

Voyons voir


La bonne réponse est 3.

Le fait est que le plus de la première ligne est déplacé vers le bas, et Kotlin pense que c'est unaryPlus (). Par conséquent, seules les trois premières cellules sont additionnées. Si nous voulons écrire ce code sur plusieurs lignes, nous devons déplacer le plus vers le haut.

Ceci est un autre des «mauvais puzzlers». N'oubliez pas que dans Kotlin, vous n'avez pas besoin de transférer la déclaration vers une nouvelle ligne, sinon elle peut la considérer comme unaire.



Je n'ai pas vu de situations où unaryPlus est nécessaire dans le vrai code sauf DSL. C'est un sujet très étrange.

C'est le prix à payer pour l'absence de point-virgule. S'ils l'étaient, il serait clair quand une expression se termine et une autre commence. Et sans eux, le compilateur doit prendre la décision. Les sauts de ligne pour le compilateur signifient très souvent qu'il est logique d'essayer d'examiner les lignes séparément.

Mais il existe un langage JavaScript très cool dans lequel vous ne pouvez pas non plus écrire de points-virgules, et ce code fonctionnera toujours correctement.

Pazzler numéro 5


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

Ce casse-tête est présenté par le conférencier de KotlinConf, Thomas Nild.

Kotlin a une grande fonctionnalité de types nullable. Nous avons un x nul, et nous pouvons le convertir, s'il s'avère nul, via l'opérateur Elvis en une valeur normale.

Que va-t-il se passer?

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

Lancez!


Le problème est à nouveau dans l'ordre ou la priorité des opérateurs. Si nous reformatons ceci, alors le format officiel fera ceci:

 val sum = x ?: 0+y 

Le format suggère déjà que 0 + y commence en premier, puis seulement x?:. Par conséquent, bien sûr, 2 reste, car X est deux, il n'est pas nul.

Pazzler numéro 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 } } 

Quand ils m'ont appelé ici, ils m'ont promis de la bière artisanale. Je vais le chercher ce soir, je ne l'ai pas encore vu. Kotlin a un excellent sujet - les constructeurs. Avec quatre lignes de code, nous écrivons notre DSL puis le créons via les générateurs.

Nous créons, tout d'abord, de l'IPA, ajoutons du houblon appelé Cascade, 100 grammes à la 15e minute de cuisson, puis imprimons cette recette. Qu'avons-nous fait?

a) Recette (nom = IPA simple, houblon = [Houblon (nom = Cascade, atMinute = 15, grammes = 100)])
b) IllegalArgumentException
c) Non compilé
d) Aucune de ces réponses

Lancez!


Nous avons obtenu quelque chose de similaire à de la bière artisanale, mais il n'y a pas de houblon, elle a disparu. Ils voulaient une IPA, mais ont obtenu Baltic 7.

C'est là que le choc des noms s'est produit. Le champ dans Hops est en fait appelé kind, et dans la ligne name = ″ Cascade ″, nous utilisons name, qui est mis en cache avec le nom de la recette.

Nous pouvons créer notre propre annotation BeerLang et l'enregistrer dans le cadre du BeerLang DSL. Maintenant, nous essayons d'exécuter ce code, et il ne doit pas être compilé avec nous.



On nous dit maintenant qu'en principe, le nom ne peut pas être utilisé dans ce contexte. Pour cela, DSLMarker est nécessaire car le compilateur à l'intérieur du générateur ne nous a pas permis d'utiliser le champ externe, si nous en avons le même à l'intérieur afin qu'il n'y ait pas de conflit de dénomination. Le code est fixé comme ça, et nous obtenons notre recette.



Pazzler numéro 7



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

Ce casse-tête est l'un des employés de JetBrains. Kotlin a une fonctionnalité quand. C'est pour toutes les occasions, il vous permet d'écrire du code cool, il est souvent utilisé avec des classes scellées pour la conception d'API.

Dans ce cas, nous avons une fonction f () qui prend un booléen et imprime quelque chose en fonction de true et false.

Que va-t-il se passer?

a) vrai VRAI; faux faux
b) vrai VRAI; faux VRAI
c) vrai FAUX; faux faux
d) Aucune de ces réponses

Voyons voir


Pourquoi Tout d'abord, nous calculons l'expression x == true: par exemple, dans le premier cas, ce sera true == true, ce qui signifie vrai. Et puis il y a aussi une comparaison avec le modèle que nous avons adopté quand.

Et lorsque x est défini sur false, évaluer x == true nous donnera faux, cependant, l'échantillon sera également faux - donc l'exemple correspondra à l'échantillon.

Il existe deux façons de corriger ce code, l'une consiste à supprimer «x ==» dans les deux cas:

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

La deuxième option consiste à supprimer (x) après quand. Lorsque fonctionne avec toutes les conditions, puis ne correspondra pas à l'échantillon.

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


Pazzler numéro 8


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

Kotlin a été commercialisé comme une langue «sans danger». Imaginez que nous ayons une classe abstraite, elle a un nom, et aussi une propriété qui renvoie le logo de cette langue: la première lettre du nom, juste au cas où, a fait du capital (du coup on a oublié de faire le capital initial).

Comme la langue est sans danger, nous changerons le nom et nous devrons probablement obtenir le bon logo, qui est une lettre. Qu'obtenons-nous vraiment?

a) K
b) NullPointerException
c) IllegalStateException
d) Non compilé

Lancez!


Nous avons obtenu une exception NullPointerException, que nous ne devrions pas recevoir. Le problème est que le constructeur de la superclasse est appelé en premier, le code essaie d'initialiser le logo de la propriété et prend le nom char à partir de zéro, et à ce stade, le nom est nul, donc une exception NullPointerException se produit.

La meilleure façon de résoudre ce problème est de procéder comme suit:

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

Si nous exécutons un tel code, nous obtenons "K". Maintenant, la classe de base appellera le constructeur de la classe de base, elle appellera en fait le nom du getter et récupérera Kotlin.

La propriété est une excellente fonctionnalité de Kotlin, mais vous devez être très prudent lorsque vous remplacez des propriétés, car il est très facile d'oublier, de faire une erreur ou d'assurer la mauvaise chose.


Pazzler numéro 9


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

Il y a une liste mutable de certaines choses effrayantes. Si cela vous rappelle Scala, ce n'est pas en vain, car il ressemble vraiment. Il y a une liste lambd, nous prenons deux compteurs - I et j, incrémentons puis faisons quelque chose avec eux. Que va-t-il se passer?

a) 1 1; 2 2; 3 3
b) 1 3; 2 3; 3 3
c) 3 1; 3 2; 3 3
d) aucune de ces réponses

Courons


Nous obtenons 3 1; 3 2; 3 3. Cela se produit car i est une variable, et elle conservera sa valeur jusqu'à la fin de la fonction. Et j est passé par valeur.

Si au lieu de var i = 0, il y aurait val i = 0, cela ne fonctionnerait pas, mais nous ne pourrions pas incrémenter la variable.

Ici, dans Kotlin, nous utilisons la fermeture, cette fonctionnalité n'est pas en Java. C'est très cool, mais cela peut nous mordre si nous n'utilisons pas immédiatement la valeur de i, mais la transmettons au lambda, qui démarre plus tard et voit la dernière valeur de cette variable. Et j est passé par valeur, car les variables dans la condition de boucle - elles sont les mêmes que val, elles ne changent plus leur valeur.

En JavaScript, la réponse serait «3 3; 3 3; 3 3 ”, car rien n'est transmis par la valeur.


Pazzler numéro 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) 

Nous avons une fonction foo (), prend deux booléens, les imprime, tout semble simple. Et nous avons un tas de chiffres, il reste à voir quel chiffre est plus grand que l'autre, et décider quelle option est correcte.

a) vrai, vrai
b) faux, faux
c) null, null
d) non compilé

Nous lançons


Non compilé.

Le problème est que le compilateur pense que cela est similaire aux paramètres génériques: avec <a, b>. Bien qu'il semble que «c» ne soit pas une classe, il n'est pas clair pourquoi il devrait avoir des paramètres génériques.

Si le code était comme ça, cela fonctionnerait très bien:

 foo(c > a, b > d) 

Il me semble que c'est un bug dans le compilateur. Mais quand je monte à Andrei Breslav avec un tel casse-tête, il dit "c'est parce que l'analyseur est comme ça, ils ne voulaient pas que ce soit trop lent." En général, il trouve toujours une explication pourquoi.

C'est malheureusement le cas. Il a dit qu'ils ne le répareraient pas, parce que l'analyseur
Kotlin ne connaît pas encore la sémantique. L'analyse se produit d'abord, puis elle la transmet à un autre composant du compilateur. Malheureusement, cela restera probablement ainsi. N'écrivez donc pas deux de ces crochets et aucun code au milieu!

Pazzler numéro 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″) 

Délégué est une excellente fonctionnalité dans Kotlin. Soit dit en passant, Andrei Breslav dit que c'est une fonctionnalité qu'il retirerait volontiers de la langue, il ne l'aime plus. Maintenant, peut-être, nous allons découvrir pourquoi! Et il a également dit que les objets compagnons étaient laids.

Mais les classes de données sont vraiment belles. Nous avons un conteneur de classe de données, il prend un nom et des éléments pour lui-même. Dans le même temps, dans le conteneur, nous implémentons le type d'éléments, c'est List, et nous déléguons toutes ses méthodes aux éléments.

Ensuite, nous utilisons une autre fonctionnalité intéressante - la destruction. Nous «détruisons» le nom et les éléments du conteneur et les affichons à l'écran. Tout semble simple et clair. Que va-t-il se passer?

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

Nous lançons


L'option la plus obscure est d. Il s'avère être vrai. Il s'est avéré que les articles disparaissent simplement de la collection d'articles, et non du début ou de la fin, mais seulement au milieu. Pourquoi?

Le problème de la déstructuration est qu'en raison de la délégation, toutes les collections de Kotlin sont également
ont leur propre option de déstructuration. Je peux écrire val (I, j) = listOf (1, 2), et obtenir ces 1 et 2 en variables, c'est-à-dire que List a implémenté les fonctions component1 () et
component2 ().

La classe de données a également component1 () et component2 (). Mais puisque le deuxième composant dans ce cas est privé, celui qui est public à List l'emporte, donc le deuxième élément est tiré de List, et nous arrivons ici 2. La morale est très simple: ne faites pas cela, ne faites pas cela.

Pazzler numéro 12


Le prochain casse-tête est très effrayant. C'est une personne soumise qui est en quelque sorte liée à Kotlin, donc il sait ce qu'il écrit.

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

Nous avons une fonction d'extension sur Any nullable, c'est-à-dire qu'elle peut être appliquée à n'importe quoi. C'est une fonctionnalité très utile. S'il n'est pas déjà dans votre projet, cela vaut la peine d'être ajouté, car il peut mettre tout ce que vous voulez dans n'importe quoi. Ensuite, nous prenons 42 et le jetons dans Nothing.

Eh bien, si nous voulons être sûrs d'avoir fait quelque chose d'important, nous pouvons le faire à la place !!! écrire !!!!, le compilateur Kotlin vous permet de faire ceci: si vous manquez deux points d'exclamation, écrivez au moins vingt-six.

Ensuite, nous faisons si (vrai), et puis moi-même je ne comprends rien ... Choisissons immédiatement ce qui se passe.

a) 87
b) Kotlin.Unit
c) Exception ClassCastException
d) Non compilé

Regarder


Il est très difficile de donner une explication logique. Très probablement, l'unité ici est due au fait qu'il n'y a plus rien à pousser là-bas. Ce code n'est pas valide, mais il fonctionne car nous avons utilisé Nothing. Nous avons téléchargé quelque chose dans Nothing, et il s'agit d'un type spécial qui indique au compilateur qu'une instance de ce type ne doit jamais apparaître. Le compilateur sait que s'il y a la possibilité de l'apparition de Nothing, ce qui est impossible par définition, alors vous ne pouvez pas vérifier plus loin, c'est une situation impossible.

Très probablement, c'est un bug dans le compilateur, l'équipe JetBrains a même dit que peut-être ce bug sera corrigé un jour, ce n'est pas très prioritaire. L'astuce est que nous avons trompé le compilateur ici à cause de cette distribution. Si vous supprimez la ligne 42.asGeneric <Rien> () !!! et arrêtez de tricher, le code arrêtera de compiler. Et si nous partons, le compilateur devient fou, pense que c'est une expression impossible, et bourre tout ce qui s'y trouve.

Je comprends cela. Peut-être qu'un jour l'expliquera mieux.


Pazzler numéro 13


Nous avons une fonctionnalité très intéressante. Vous pouvez utiliser l'injection de dépendance, ou vous pouvez vous en passer, créer des singletones via un objet et exécuter votre programme à la perfection. Pourquoi avez-vous besoin de Koin, Dagger ou quelque chose? Les tests, cependant, seront difficiles.

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

Nous avons une classe A ouverte à l'héritage, elle prend quelque chose en elle-même, nous créons deux objets, un singleton, B et C, tous deux héritent de A et se transmettent. Autrement dit, un excellent cycle se forme. Ensuite, nous imprimons ce que B et C ont obtenu.

a) nul; nul
b) C; nul
c) ExceptionInInitializerError
d) Non compilé

Nous lançons


L'option correcte est C; nul

On pourrait penser que lorsque le premier objet est initialisé, le second n'est pas encore là. Mais, lorsque nous en déduisons, C manque de B. Autrement dit, l'ordre inverse est obtenu: pour une raison quelconque, le compilateur a décidé d'initialiser C en premier, puis il a initialisé B avec C. Cela semble illogique, il serait logique, au contraire, nul ; B.

Mais le compilateur a essayé de faire quelque chose, il n’a pas réussi, il a laissé nul là et a décidé de ne rien nous jeter. Ça pourrait être comme ça aussi.

Le cas échéant? dans le type de paramètre, supprimer?, cela ne fonctionnera pas.



Nous pouvons dire au compilateur que lorsque null a été résolu, il a essayé, mais a échoué, mais quoi? non, il nous jette une exception qu'il est impossible de faire un cycle.

Pazzler №14


La version 1.3 a publié de superbes nouvelles coroutines dans Kotlin. J'ai longtemps réfléchi à la manière de trouver un casse-tête sur la corutine, afin que quelqu'un puisse le comprendre. Je pense que pour certaines personnes, tout code avec des coroutines est un casse-tête.

Dans la version 1.3, certains noms de fonction ont changé, comme dans la version 1.2 de l'API expérimentale. Par exemple, buildSequence () est renommé simplement sequence (). Autrement dit, nous pouvons faire d'excellentes séquences avec la fonction de rendement, des boucles infinies, puis nous pouvons essayer de tirer quelque chose de cette séquence.

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

Ils ont dit avec des coroutines que toutes les primitives sympas qui sont dans d'autres langages, comme le rendement, peuvent être faites comme des fonctions de bibliothèque, parce que le rendement est une fonction de suspension qui peut être interrompue.

Que va-t-il se passer?

a) [1, 2, 3]
b) [0, 1, 2]
c) Boucle infinie
d) Aucune de ces réponses

Lancez!


L'option correcte est la dernière.

La séquence est un engin paresseux, et quand on s'y accroche, elle est aussi paresseuse. Mais si vous ajoutez toList, il affichera vraiment [0, 1, 2].

La bonne réponse n'est pas du tout liée aux coroutines. Les coroutines fonctionnent vraiment, elles sont faciles à utiliser. Pour la fonction séquence et rendement, vous n'avez même pas besoin de connecter une bibliothèque avec des coroutines, tout est déjà dans la bibliothèque standard.

Pazzler №15


Ce casse-tête est également maîtrisé par le développeur de JetBrains. Il y a un tel code infernal:

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

Quand je l'ai vu pour la première fois, pendant KotlinConf, je ne pouvais pas dormir, j'ai essayé de comprendre ce que c'était. Un tel code cryptique peut être écrit dans Kotlin, donc si quelqu'un pensait que Scalaz était effrayant, alors dans Kotlin c'est également possible.

Imaginons:

a) Kotlin.Unit
b) Kotlin.Rien
c) Non compilé
d) Aucune de ces réponses

Courons


Nous avons obtenu une unité qui est venue de nulle part.

Pourquoi? Nous attribuons d'abord la variable lambda: {->} - c'est un code valide, vous pouvez écrire un lambda vide. Il n'a pas de paramètres, il ne renvoie rien. En conséquence, il renvoie Unit.

Nous attribuons un lambda à la variable et écrivons immédiatement l'extension à ce lambda, puis l'exécutons. En fait, il réservera simplement Kotlin.Unit.

Ensuite, sur cette lambda, vous pouvez écrire une fonction d'extension:

 .fun Function<*>.(){} 

Il est déclaré sur le type Function <*>, et ce que nous avons en haut lui convient également. En fait, c'est Function <Unit>, mais je n'ai pas écrit Unit que ce n'était pas clair. Savez-vous comment fonctionne un astérisque dans 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/fr440974/


All Articles