Où les programmeurs Java ont tendance à trébucher sur Kotlin

Kotlin sera la langue officielle d'Android a été annoncé à Goole I / O 2017. Je suis sûr que de nombreux programmeurs Java lanceront Kotlin à l'avenir, donc cet article expliquera ce que les programmeurs Java ont tendance à trébucher dans Kotlin.

Cet article est écrit de manière à pouvoir être compris indépendamment, mais c'est le deuxième de la série ci-dessous. Comme il est supposé que vous comprenez la syntaxe de base de Kotlin, veuillez consulter "Presque la même chose que Java" pour les bases de Kotlin. ..

  1. Presque identique à Java
  2. ** Lieux qui nécessitent une nouvelle façon de penser et ont tendance à trébucher ← Contenu traité dans cet article **
  3. Éléments pratiques propres à Kotlin

Où vous avez besoin d'une nouvelle façon de penser et avez tendance à trébucher

** Lorsque vous apprenez un nouveau concept, vous ne pouvez pas bien l'utiliser sans savoir non seulement ce que vous pouvez faire, mais aussi pourquoi. ** Dans cette section, j'expliquerai ce que je pense est particulièrement important dans le fait que Kotlin est différent de Java et nécessite une nouvelle façon de penser. ** Basé sur "Pourquoi est-ce ainsi?", "Que puis-je faire?" "Explique**.

Smart Cast

Je ne sais pas si c'est une idée omniprésente en général, mais je pense qu'il y a une tendance en Java selon laquelle «l'instance» n'est pas bonne. L'idée est que la ramification à «l'instance de» puis la distribution est un dernier recours, et il est souhaitable de faire quelque chose avec le polymorphisme. Cependant, il y a un bond en avant que la solution à la mauvaise «instance» est le polymorphisme. Il devrait y avoir d'autres solutions.

Le problème avec ʻinstanceof` de Java est que vous ne pouvez pas vérifier et convertir les types en même temps.

// Java
Animal animal = new Cat();

if (animal instanceof Cat) {
  Cat cat = (Cat)animal; //Abattu
  cat.catMethod(); // `Cat`Traitement qui utilise la méthode de
}

Une fois, j'ai confirmé dans ʻinstanceof que ʻanimal était Cat, mais ensuite j'ai dû abattre vers Cat. Le downcasting est un processus dangereux et je ne veux pas l'utiliser si possible. Si vous faites une erreur et utilisez (Dog) animal ici, vous obtiendrez une ClassCastException lors de l'exécution.

Ce qui est amusant, c'est que la précédente déclaration ʻif confirme que ʻanimal est Cat, mais vous devez le convertir explicitement. La portée de ʻif garantit que ʻanimal est Cat, donc ce serait bien si le compilateur pouvait traiter ʻanimal comme Cat`. C'est exactement ce que fait ** _Smart Cast _ ** de Kotlin.

//Kotlin
val animal = Cat()

if (animal is Cat) { // Smart Cast
  animal.catMethod() // `Cat`Traitement qui utilise la méthode de
}

Smart Cast lui-même ressemble à un peu une fonctionnalité, mais avec Smart Cast, Kotlin utilise normalement le cast (par Smart Cast). En particulier, il est indispensable pour Null Safety, qui sera expliqué plus loin, et la Classe Sealed, qui sera expliquée la prochaine fois, est également compatible avec Smart Cast. Il est nécessaire de rompre avec l'idée qu'il n'est pas bon de vérifier le type et la branche.

Null Safety

La spécification de langage sur laquelle les programmeurs Java sont le plus confus et que je trouve la plus merveilleuse dans Kotlin est ** Null Safety **.

En Java, un code comme ↓ peut provoquer une NullPointerException.

// Java
String s = foo(); //Peut être nul
int l = s.length(); //NullPointerException au moment de l'exécution si nul

Cependant, l'écriture de code similaire dans Kotlin entraîne une erreur de compilation.

// Kotlin
val s = foo() //Peut être nul
val l = s.length //Erreur de compilation

Le compilateur Kotlin détecte le code qui peut provoquer une erreur au moment de l'exécution et vous en informe au moment de la compilation. En général, ** plus la détection d'une erreur est retardée, plus elle est difficile à gérer. Si les erreurs d'exécution ne peuvent pas être détectées par les tests, elles peuvent être intégrées au produit en tant que bogues potentiels et libérées. S'il s'agit d'une erreur de compilation, elle ne sera pas publiée sans avoir été résolue. ** **

Cette erreur de compilation peut être éliminée en cochant «null».

// Kotlin
val s = foo() //Peut être nul
if (s != null) {
  val l = s.length // OK
}

C'est là que Smart Cast est utile. La vérification «null» garantit que «s» n'est pas «nul», et dans le cadre de ce «si», «s» peut être traité comme une «chaîne» qui n'est pas «nulle».

Alors, quel type est Smart Cast cast à quel type dans ce cas?

Kotlin traite les types de valeurs possibles comme «null» et les types de valeurs qui sont garanties non nulles ». Le premier est appelé ** _Nullable Type _ ** et le second est appelé ** _ Non-null Type _ **.

Comme avec Java, si vous écrivez String etc., ce sera de type non nul. Dans ce cas, vous ne pouvez pas remplacer «null».

// Kotlin
val s: Sting = null //Erreur de compilation

Si vous ajoutez ? Au type d'origine et écrivez String? Etc., il devient Nullable Type. Vous pouvez attribuer null à Nullable Type.

// Kotlin
val s: String? = null // OK

Il est important de noter que «String» et «String?» Sont des types complètement différents. ** Vous ne pouvez pas appeler la méthode de String sur une valeur de type String?. ** Dans le premier exemple, s.length provoque une erreur de compilation, non pas parce que s peut être null, mais ** le type String? A des propriétés telles que length`. Pas parce que **. C'est «String» qui a «length», pas «String?».

Et une autre chose importante est que ** String est un sous-type de String?. ** Par conséquent, vous pouvez attribuer du type String au type String?, Mais vous ne pouvez pas affecter du typeString?Type au type String.

// Kotlin
val a: String = "abc"
val b: String? = a // OK

val c: String? = "xyz"
val d: String = c //Erreur de compilation

C'est la même chose que d'assigner de Cat à ʻAnimal` mais pas l'inverse.

// Kotlin
val a: Cat = Cat()
val b: Animal = a // OK

val c: Animal = Animal()
val d: Cat = c //Erreur de compilation

Donc, pour revenir à l'histoire originale, quel type Smart Cast avec le test null a-t-il converti en quel type? La réponse est, je lance String? En String.

// Kotlin
val s: String? = foo() //ici`String?`
if (s != null) { //Dans ce cadre`s`Est`String`
  val l = s.length
}

En d'autres termes, s! = Null fonctionne comme s is String. En fait, vous pouvez obtenir le même résultat en écrivant comme ↓.

// Kotlin
val s: String? = "abc"
if (s is String) { //Smart Cast avec est au lieu d'une vérification nulle
  val l = s.length
}

C'est ainsi que Kotlin empêche NullPointerException. Si vous écrivez Java, il est normal de rencontrer «NullPointerException». Ne serait-il pas génial d'avoir Null Safety là où cela ne se produit pas?

!!

Null Safety est génial, mais le type n'est pas polyvalent.

Par exemple, la List de Kotlin (pour être exact, ʻIterable) a une méthode appelée max. Cela renverra le plus grand élément de List`.

// Kotlin
listOf(2, 7, 5, 3).max() // 7

Cependant, la valeur de retour de la méthode max de List <Int> ʻ n'est pas ʻInt. «Int?». Ceci est dû au fait que la «Liste» vide n'a pas le plus grand élément.

// Kotlin
listOf<Int>().max() // null

Supposons maintenant que vous ayez toujours une List <Int> qui contient un ou plusieurs éléments, et que vous vouliez obtenir le plus grand élément et le mettre au carré. En Java, il est possible de mettre au carré la valeur de retour de max sans réfléchir.

// Java
List<Integer> list = ...; //Doit contenir un ou plusieurs éléments
int maxValue = Collections.max(list);
int square = maxValue * maxValue; // OK

Mais Kotlin ne fonctionne pas.

// Kotlin
val list: List<Int> = ...; //Doit contenir un ou plusieurs éléments
val maxValue = list.max()
val square = maxValue * maxValue //Erreur de compilation

C'est parce que le type de maxValue dans ↑ est ʻInt?, Pas ʻInt. Il n'y a pas d'opérateur * qui calcule entre ʻInt? `.

Cocher null pour résoudre ce problème entraînera un code terrible.

// Kotlin
val list: List<Int> = ...; //Doit contenir un ou plusieurs éléments
val maxValue = list.max() //maxValue est`Int?`Moule
if (maxValue != null) { //Dans ce cadre`maxValue`Est`Int`Moule
  val square = maxValue * maxValue // OK
} else {
  throw RuntimeException("Never reaches here.") //Ne viens jamais ici
}

Vous pourriez vous demander s'il y a une clause ʻelse, mais si vous n'écrivez pas ceci, vous risquez de réduire l'erreur si la liste` est vide à cause d'un bogue. ** Les erreurs d'écrasement sont la pire chose que vous puissiez faire pour retarder la recherche d'un bogue plutôt qu'une erreur d'exécution. ** **

Cependant, il est difficile d'écrire à chaque fois une vérification null inutile comme ↑. Il s'agit d'un processus inutile en Java. Si vous devez écrire ce code à chaque fois pour Null Safety, alors Null Safety n'est pas très bon. En outre, le traitement gênant a tendance à être omis. Certaines personnes n'écriront pas la clause ʻelse`. Ensuite, l'erreur est écrasée et c'est le pire.

Le problème avec ce problème est qu'il est difficile à résoudre avec un type. Kotlin n'a pas de type comme "une liste qui contient toujours plus d'un élément", et même si c'est le cas, il y a des cas où cela ne peut pas être résolu.

// Kotlin
val list: MutableList<Int> = mutableListOf() //Vide à ce stade

...

list.add(42)
val maxValue = list.max() //Ce n’est certainement pas vide ici`maxValue`Est`Int?`

Dans la dernière ligne de ↑, list n'est absolument pas vide, mais la substance de list est juste une instance de MutableList. Même s'il existe un type comme «NonEmptyMutableList» (liste non vide), il ne devient pas une instance de «NonEmptyMutableList» lorsque l'instance de «MutableList» n'est pas vide.

Un opérateur postfix appelé «!!» gère ces problèmes. !! convertira T? en T. Cependant, si la valeur est «null», elle lève une exception.

// Kotlin
val list: List<Int> = ...; //Doit contenir un ou plusieurs éléments
val maxValue = list.max()!! // `maxValue`Est`Int`Moule
val square = maxValue * maxValue // OK

!! lève une NullPointerException si elle échoue à non-null, ce qui perfore la sécurité de Null Safety. N'en abusez jamais. N'utilisez !! que si vous savez qu'il n'est pas ** null et qu'il n'échouera jamais, comme dans l'exemple ci-dessus ** [^ 1]. Bien qu'il soit Nullable sur le type, il ne doit être utilisé que lorsque l'on peut dire qu'il n'est absolument pas «nul» en raison du flux de traitement.

?.

Vous pouvez utiliser ? . Pour appeler une méthode de T pour une valeur de type T? ʻSeulement si ce n'est pas null`.

// Kotlin
val s: String? = ...
val l: Int? = s?.length

Notez que le ↑ l est ʻInt?. Si «s» est «nul», le résultat de «s» .Length »est« nul ». Par conséquent, la valeur de retour de length est ʻInt, mais dans s? .Length, il s'agit de type Nullable ʻInt?`.

? . Est pratique car il peut être écrit comme foo? .Bar? .Baz lors de l'écriture de plusieurs processus qui peuvent être null. Cependant, abuser de «?.» Ne donne pas de très bons résultats. En particulier, nous vous déconseillons de traiter les valeurs comme ** Nullable. ** Si vous essayez d'utiliser une valeur et qu'elle est "null", vous ne connaissez pas la source de "null". Si la raison de devenir «null» est due à un bogue, il est difficile de corriger le bogue car vous ne savez pas où le «null» s'est produit. Pour éviter une telle situation, il est recommandé de vérifier les ** valeurs Nullable dès null et de les rendre non nulles. ** **

?:

Il arrive souvent que vous souhaitiez renseigner la valeur initiale s'il n'y a pas de valeur. Dans de tels cas, l'opérateur ?: Est utile. ?: Est écrit sous la forme foo ?: Bar, et si foo est null, il devient bar à la place.

Par exemple, si vous souhaitez afficher Aucun nom lorsque le nom de l'utilisateur n'est pas défini, vous pouvez écrire comme ↓.

val name: String? = ...
val displayName: String = name ?: "No Name"

L'important ici est que le type de «displayName» soit «String» au lieu de «String?». ?: Utilise la chaîne alternative " No Name " si name est null, donc ce n'est toujours pas null. Par conséquent, le type de résultat est «String» au lieu de «String?».

Encore plus pratique est la possibilité d'écrire des instructions telles que «return» sur le côté droit de «?:». Il est difficile d'écrire avec la vérification null par ʻif`, et c'est utile parce que l'échappement précoce peut être réalisé en même temps que la substitution.

fun foo(list: List<String>) {
  val length: Int = list.min()?.length ?: return // `list`S'échapper tôt s'il est vide
  //Traitement qui utilise la longueur
}

?.let

Depuis que j'ai reçu le commentaire de ↓, je vais expliquer brièvement la méthode qui peut être utilisée dans un tel cas.

kitakokomo Pour Nullable, "list Of (1,2) list [0] +5 fonctionne bien, mais mapOf ( 1 à 11, 2 à 22) map [1] +5 est Nullable et c'est triste que cela ne fonctionne pas. "

? . Peut être utilisé pour appeler des propriétés et des méthodes sous la forme de foo? .Bar, mais les valeurs Nullable ne peuvent pas être passées comme arguments ou opérandes d'opérateurs. La méthode let peut être utilisée dans de tels cas (je vais expliquer la méthode let en détail la prochaine fois, donc je ne présenterai cette méthode qu'ici).

// Kotlin
map[1]?.let { it + 5 }

De cette façon, le résultat de l'ajout des valeurs de retour de map [1], ʻInt? Et 5, peut être obtenu comme ʻInt? . Cependant, il n'est pas lisible et des abus peuvent conduire à la gestion des valeurs comme Nullable, alors utilisez-le au minimum et vérifiez immédiatement le résultat du calcul ?: Nous vous recommandons de le rendre non nul.

En passant, l'opérateur de Kotlin est un sucre de syntaxe de méthode (que j'expliquerai la prochaine fois), donc si vous voulez juste additionner, utilisez la méthode plus, qui est le corps de +, comme ↓ Vous pouvez également écrire.

map[1]?.plus(5)

Retour en ligne et non local

En plus de Null Safety, les programmeurs Java peuvent trouver le retour non local avec l'expansion en ligne ennuyeux.

Dans Kotlin, les méthodes avec le modificateur ʻinline` sont développées en ligne. L'expansion en ligne est un mécanisme par lequel le compilateur étend l'implémentation de méthode à l'appelant pour réduire la surcharge de l'appel de méthode.

↓ est une image de l'expansion en ligne.

// Kotlin
//Avant le déploiement en ligne
inline fun square(value: Int): Int {
  return value * value
}

for (i in 1..100) {
  println(square(i))
}
//Après le déploiement en ligne
for (i in 1..100) {
  println(i * i)
}

Mais ce qui est important dans Kotlin, c'est que l'expression lambda passée à l'argument de la méthode ʻinline est également étendue à ʻinline.

// Kotlin
//Avant le déploiement en ligne
inline fun forEach(list: List<Int>, operation: (Int) -> Unit) {
  for (element in list) {
    operation(element)
  }
}

val list: List<Int> = ...
forEach(list) { element ->
  println(element)
}
//Après le déploiement en ligne
val list: List<Int> = ...
for (element in list) {
  println(element)
}

Cela donne les mêmes performances que celles écrites dans une boucle en utilisant forEach, map, filter, fold (fold est équivalent à reduction, qui donne la valeur initiale de Java), etc. Cela peut être réalisé. ↑ J'ai créé forEach par moi-même, mais même dans la bibliothèque standard, toutes les méthodes telles que forEach, map, filter et fold of List sont ʻinline`.

«En ligne» de Kotlin est censé être utilisé en conjonction avec des expressions lambda comme celle-là, et lorsque vous essayez de «insérer» une fonction ou une méthode ordinaire comme ↑ «square», le compilateur ressemble à ↓ Je vais émettre un avertissement.

Expected performance impact of inlining 'public inline fun square(value: Int): Int defined in root package' can be insignificant. Inlining works best for functions with lambda parameters

Jusqu'à présent, ce n'est pas une histoire spéciale. La chose la plus déroutante pour les programmeurs Java est un mécanisme appelé ** _Non-local return _ **. Le retour non local est un mécanisme qui vous permet de «renvoyer» la méthode en dehors de l'expression lambda à partir de l'expression lambda, ou de «casser» la boucle en dehors de l'expression lambda.

Ceci est également expliqué par un exemple. Considérons une méthode ʻand qui prend le produit logique (&&) de tous les éléments de List `. Par exemple, si «[vrai, vrai, vrai]», le résultat de «et» est «vrai», mais si même un «faux» est mélangé comme «[vrai, faux, vrai]», le résultat est «faux. Cela devient ".

La méthode ʻandest facile à implémenter dans l'instruction étenduefor`.

// Java
static boolean and(List<Boolean> list) {
  for (boolean x : list) {
    if (!x) {
      return false;
    }
  }
  return true;
}

Cependant, vous ne pouvez pas faire la même chose en Java avec forEach, qui est presque équivalent à for. En effet, il n'est pas possible de `renvoyer 'une méthode en dehors de l'expression lambda dans l'expression lambda.

// Java
static boolean and(List<Boolean> list) {
  list.forEach(x -> {
    if (!x) {
      return false; //Erreur de compilation
    }
  });
  return true;
}

Kotlin permet cela.

// Kotlin
fun and(list: List<Boolean>): Boolean {
  list.forEach { x ->
    if (!x) {
      return false // OK
    }
  }
  return true
}

Il s'agit d'un retour non local.

Il est désagréable de pouvoir «renvoyer» l'extérieur de l'expression lambda à partir de l'expression lambda. Comment pouvez-vous faire cela? C'est parce que «forEach» est en ligne. L'image après le développement en ligne du code dans ↑ est ↓.

// Kotlin
fun and(list: List<Boolean>): Boolean {
  for (x in list) {
    if (!x) {
      return false
    }
  }
  return true
}

Il n'est pas étonnant que vous puissiez «revenir» dans ce cas. Le return dans ʻif qui était à l'origine dans l'expression lambda est maintenant le return dans ʻand qui était en dehors de l'expression lambda.

Dans Kotlin, «return» écrit dans une expression lambda agit toujours à l'extérieur de l'expression lambda. Vous ne pouvez pas utiliser return pour renvoyer la valeur de retour de l'expression lambda elle-même. La valeur de retour de l'expression lambda sera la valeur de la dernière expression évaluée dans l'expression lambda.

Alors que faire si vous écrivez return dans une expression lambda passée à une méthode qui n'est pas ʻinline`? Il ne se développe pas en ligne, il ne peut donc pas agir à l'extérieur de l'expression lambda. Dans ce cas, une erreur de compilation se produira.

// Kotlin
// `inline`Si non
fun foo(f: () -> Int): Int {
  ...
}

fun bar(): Int {
  return foo {
    if (flag) {
      return 0 //Erreur de compilation
    }

    42
  }
}

Dans l'exemple de ↑, il n'y a pas de problème si foo est ʻinline` comme ↓.

// Kotlin
// `inline`dans le cas de
inline fun foo(f: () -> Int): Int {
  ...
}

fun bar(): Int {
  return foo {
    if (flag) {
      return 0 // OK: `bar`de`return`
    }

    42
  }
}

** return dans une expression lambda agit toujours en dehors de l'expression lambda ** est une spécification assez drastique d'autres langages, y compris Java, mais voici où traiter l'expression lambda de Kotlin. Si vous ne le maintenez pas enfoncé, vous en serez accro.

Le retour non local est désagréable au début, mais utile selon la façon dont vous l'utilisez. Par exemple, en Java, vous pouvez facilement créer une portée anonyme avec {}, mais dans Kotlin {} est assignée à une expression lambda et vous ne pouvez pas créer une portée anonyme. Si vous n'avez pas de portée anonyme, vous aurez des problèmes lorsque vous voudrez couper la portée en plus petits morceaux pour éviter les noms de variables en double. Kotlin utilise «run» dans de tels cas.

// Java
for (int x : list) {
  { //Portée anonyme
    int a = 2;
    ...
    if (x < a) {
      break;
    }
  }

  {
    int a = 3; //Parce que la portée est différente`a`Peut être fait
    ...
  }
}
// Kotlin
for (x in list) {
  run { //Au lieu d'une portée anonyme
    val a = 2
    ...
    if (x < a) {
      break //Style Lambda mais boucle extérieure`break`ça peut
    }
  }

  run {
    val a = 3 //Parce que la portée est différente`a`Peut être fait
    ...
  }
}

De cette façon, vous pouvez utiliser le retour non local pour créer quelque chose comme une syntaxe de contrôle personnalisée. Un abus peut altérer la lisibilité, mais c'est une bonne caractéristique que si vous voulez une nouvelle syntaxe de contrôle, vous pouvez la résoudre avec le mécanisme de langage au lieu de gonfler la spécification du langage. Entendons-nous bien avec le retour non local.

Fonction anonyme

Si vous ne pouvez pas return dans une expression lambda, que faire si vous voulez vous échapper tôt?

// Kotlin
numbers.forEach { number ->
  if (number % 2 == 1) {
    //Je veux m'échapper tôt ici, que dois-je faire?
  }

  ...
}

Dans ce cas, il est préférable d'utiliser ** _ Fonction anonyme _ ** au lieu de l'expression lambda.

// Kotlin
numbers.forEach(fun(number: Int) {
  if (number % 2 == 1) {
    return //Évasion précoce
  }

    ...
})

Contrairement aux expressions lambda, «return» dans une fonction anonyme signifie «retour» dans la fonction anonyme elle-même.

ʻAnyet ʻAny?

En Java, la classe racine était ʻObject. À Kotlin, c'est «N'importe». Lorsque vous utilisez des méthodes Java de Kotlin, partout où ʻObject est utilisé dans les arguments et les valeurs de retour en Java, Kotlin le fait ressembler à ʻAny. Mais ce n'est pas seulement un changement de nom. Il est possible d'utiliser l''Object de Java dans Kotlin [^ 2]. Dans Kotlin, ʻObject n'est pas la classe racine et ʻAny est une superclasse de ʻObject`.

Pourquoi avez-vous besoin de ʻAny pour? En Java, l'attribution d'un type primitif à ʻObject nécessitait autrefois une boxe à une classe wrapper telle que ʻInteger. Il n'y a pas de dérivation basée sur le type entre ʻint et ʻObject. Cependant, ʻInt de Kotlin et d'autres se comportent comme des classes, donc ʻInt est un sous-type de ʻAny. Par conséquent, vous pouvez simplement affecter une instance de la classe ʻInt à une variable de type ʻAny.

// Kotlin
val a: Int = 42
val b: Any = a
if (b is Int) { //Directement`Int`Vérifiez s'il s'agit d'une instance de
  //Dans ce cadre`b`À`Int`Peut être utilisé comme
  println(b + 1) // 43
}

Cependant, cela ne signifie pas qu'il n'y a pas de boxing interne au moment de l'exécution. Je n'ai rien trouvé de mentionné directement dans la documentation, mais les affectations à ʻAny, telles que ʻInt, sont, en principe, [comportement pour les affectations à Nullable Type](https: // Par analogie avec kotlinlang.org/docs/reference/basic-types.html#representation), il semble qu'il sera encadré en interne. Contrairement à Java, il peut être utilisé syntaxiquement sans être conscient du boxing [^ 3], mais sachez qu'affecter ʻInt à ʻAny a une surcharge de performances.

En Java, si vous ne spécifiez pas explicitement une superclasse avec ʻextends lors de la déclaration d'une classe, cette classe héritera de ʻObject. De même, Kotlin hérite de ʻAny` si vous ne spécifiez pas de superclasse.

Cependant, ce qui est déroutant, c'est que Kotlin a une valeur qui ne peut pas être attribuée à ** ʻAny **. C'est «nul». Cela signifie que ʻAny n'est pas un supertype de type Nullable, tel que ʻInt? . ** Dans Kotlin, le type supérieur qui existe à la racine de la hiérarchie des types est ʻAny? . ** Il n'y a rien qui ne puisse être assigné à «N'importe? ʻAny` est un nom qui peut être attribué à n'importe quoi, mais c'est déroutant, mais la hiérarchie des types peut être représentée graphiquement comme suit [^ 4].

                 [Any?]
                   |
           +---------------+
           |               |
         [Any]  [Tous les autres Nullable]
           |
[Tous les autres Non-null]

Le plus difficile si vous ne comprenez pas cela, c'est lorsque vous utilisez Generics. Si vous comprenez que «?» Est juste un symbole qui vous permet de mettre un «nul», vous avez tendance à implémenter quelque chose comme ↓.

// Kotlin
//Je veux aussi pouvoir passer le type Nullable
fun <T> id(x: T?): T? {
    return x
}

Cependant, le ** ↑ ? N'est pas nécessaire. ** Parce que «T» peut représenter un type Nullable. Les paramètres de type agissent comme des espaces réservés pour tous les types, y compris le type Nullable, s'ils ne sont pas contraints (la limite supérieure est ʻAny? `Par défaut).

// Kotlin
//Vous pouvez toujours transmettre la valeur de type Nullable
fun <T> id(x: T): T {
    return x
}

Et si vous voulez pouvoir passer uniquement la valeur de Type non nul, ce sera comme ↓.

// Kotlin
//Le type Nullable ne peut pas être passé
fun <T: Any> id(x: T): T {
    return x
}

C'est une contrainte que seul le type à la fin de la branche ʻAnysur la gauche du diagramme hiérarchique de types précédent peut être considéré comme unT (la limite supérieure est ʻAny). Ce n'est pas différent de contraindre quelque chose comme «<T: Animal>». Si vous ne comprenez pas le sous-typage des types Nullable et Non Null et comprenez que ? Est un symbole qui vous permet de mettre null, pourquoi T: Any> Vous ne comprendrez pas si `représentera désormais un type non nul.

"Rien" et "Rien?"

Nothing est un compagnon de ʻAny et ʻAny?. «Rien» est appelé le type inférieur et est un sous-type de tous les types. Cela signifie que toutes les classes sont héritées plusieurs fois et que n'importe quelle méthode peut être appelée. Bien sûr, un tel type n'est pas faisable, il n'y a donc aucune instance de "Nothing". Que signifie penser à un tel motif fantastique?

Kotlin vous permet de spécifier Nothing comme type de retour de la méthode. Cependant, comme mentionné ci-dessus, il n'y a aucune instance de "Nothing". Si vous spécifiez le type «Nothing» comme type de retour, mais que vous ne pouvez pas «retourner» la valeur du type «Nothing», vous obtiendrez une erreur de compilation. La seule façon de l'obtenir à travers la compilation est de lancer une exception dans la méthode. Par conséquent, dans Kotlin, une méthode qui renvoie «Rien» échoue toujours à indiquer qu'elle «lèvera» une exception.

Ceci est utile pour les vérifications d'exhaustivité du compilateur. Par exemple, supposons que vous ayez une méthode appelée ʻalwaysFailqui échoue toujours etlève` une exception. Considérez le code suivant.

// Java
static void alwaysFail() {
  throw new RuntimeException();
}

static String foo(int bar) {
  switch (bar) {
    case 0:
      return "A";
    case 1:
      return "B";
    case 2:
      return "C";
    default:
      alwaysFail();
  }
} //Erreur de compilation

ʻAlwaysFail fait toujours une exception throw, donc la fin de cette méthode n'est jamais atteinte. Cependant, syntaxiquement, la fin de la méthode est atteinte dans le cas de default, donc foo entraînera une erreur de compilation car certains flux n'ont pas de valeur de retour return`.

Dans Kotlin, en rendant la valeur de retour de ʻalwaysFail de type Nothing`, le compilateur détermine que le traitement ultérieur ne sera pas exécuté, et la compilation passera même dans le cas comme ↑.

// Kotlin
fun alwaysFail(): Nothing {
    throw RuntimeException()
}

fun foo(bar: Int): String {
    when (bar) {
        0 -> return "A"
        1 -> return "B"
        2 -> return "C"
        else -> alwaysFail()
    }
}

C'est pratique!

Au fait, c'est un peu un aparté, mais s'il y a «N'importe?», Il ne devrait y avoir «Rien?». Que signifie "Rien?"

"Rien?" Indique qu'il s'agit de "Rien" ou "null". La seule instance de "Nothing?" Est "null" car il n'y a aucune instance de "Nothing". Autrement dit, "Nothing?" Est un type qui ne peut contenir que "null". Dans un sens, null est comme une instance de la classe Nothing? (Bien sûr, il n'y a vraiment pas de classe appeléeNothing?).

"Rien?" Est aussi un sous-type de tous les types Nullable. Cela ressemble à ↓ sur la figure.

                 [Any?]
                   |
           +---------------+
           |               |
         [Any]  [Tous les autres Nullable]
           |               |
[Tous les autres Non-null]   |
           |               |
           |           [Nothing?]
           |               |
           +-------+-------+
                   |
               [Nothing]

Je ne pense pas que vous ayez beaucoup de chances d'utiliser Nothing?, Mais si vous voulez utiliser un type qui ne peut contenir que null, il peut être utile de ne pas oublier d'utiliserNothing?.

Génériques et dégénérescence

Si vous ne connaissez pas les jokers et les modifications Java, vous ne pourrez pas comprendre le contenu de ↓. Dans ce cas, nous vous recommandons de lire d'abord ici.

Java utilise des caractères génériques comme List <? Extends Animal> pour contrôler la qualification du côté qui utilise le type générique. Il s'agit d'une méthode appelée Variance du site d'utilisation. D'autre part, C # et Scala ont profité de la réflexion de Java et ont adopté une méthode appelée Variance du site de déclaration. La variance du site de déclaration, comme son nom l'indique, contrôle la dégénérescence du côté déclarant, et non du côté qui utilise le type. Kotlin peut faire les deux, mais en gros, il est préférable de le concevoir de manière à ce qu'il ne puisse être géré que par la variance du site de déclaration. La bibliothèque standard de Kotlin utilise également la variance du site de déclaration partout.

Examinons un exemple concret de la variance du site de la Déclaration.

Par exemple, ʻIterable utilise le paramètre de type Tuniquement dans la valeur de retour. Dans ce cas, il serait pratique que l'utilisateur n'ait pas à faire ʻIterable <? Extends Animal>(Java) / ʻIterable (Kotlin), mais leT` est covariant depuis le début.

D'un autre côté, Comparable <T> utilise le paramètre de type T uniquement comme argument. Dans ce cas, il serait pratique que l'utilisateur n'ait pas à faire Comparable <? Super Cat> (Java) / Comparable <in Cat> (Kotlin), mais le T est rebelle depuis le début.

Pour y parvenir, les éléments ʻIterable et Comparable de Kotlin ajoutent ʻout et ʻin` lors de la déclaration des paramètres de type pour spécifier la co-modification et l'anti-modification. Il s'agit de la variance du site de déclaration.

// Kotlin
interface Iterable<out T> {
  ...
}

interface Comparable<in T> {
  ...
}

En ajoutant ʻout ou ʻin lors de la déclaration d'un paramètre de type de cette manière, ? Etend / ʻout ou? Il se comportera ainsi. De plus, par exemple, si vous essayez d'utiliser T comme argument de méthode dans ʻIterable ` [^ 5], le compilateur vous indiquera une erreur de compilation.

Le concepteur de la classe ou de l'interface pense plus au type que l'utilisateur et devrait être en mesure de définir la modification de manière appropriée. Inversement, penser à la dénaturation au moment de la déclaration empêche également la conception de types impairs qui négligent la dénaturation. Il est également gênant d'ajouter ? Extends / ʻout` etc. à chaque fois que vous l'utilisez. La variance du site de déclaration est une approche des problèmes que présentent de telles variances du site d'utilisation.

Conversion explicite des types primitifs

En Java, ↓ est un bon code.

// Java
int x = 42;
double y = x; // OK

Cependant, Kotlin donne une erreur de compilation.

// Kotlin
val x: Int = 42
val y: Double = x //Erreur de compilation

En général, «A» doit être un sous-type de «B» pour pouvoir attribuer une valeur de type «A» à une variable de type «B». Cependant, ʻIntde Kotlin n'est pas une sous-classe deDouble. S'il n'y a pas de dérivation de type et que vous pouvez l'affecter, cela signifie qu'une conversion de type implicite a été effectuée. Kotlin ne fait pas de conversion de type implicite, même si elle passe de ʻInt à Double. Cela peut sembler fastidieux au début si vous êtes habitué à Java, mais je pense que c'est une bonne spécification car cela ne rend pas la gestion de type appropriée.

Par exemple, exécuter ↓ en Java provoque le débordement de «short» et l'affichage de «-32768».

// Java
short a = Short.MAX_VALUE;
a++;
System.out.println(a); // -32768

Alors, qu'est-ce qui est affiché dans ↓?

// Java
short a = Short.MAX_VALUE;
short b = 1;
System.out.println(a + b);

Dans ce cas, il s'agit de «32768». Ceci est dû au fait que ʻaetb sont implicitement convertis en ʻint avant que l'ajout de ʻa + b` ne soit effectué. La conversion de type implicite a tendance à provoquer un tel comportement inattendu si les spécifications ne sont pas bien comprises. Je pense que c'est une meilleure approche pour fournir une méthode concise de conversion de type explicite que la conversion de type implicite.

Kotlin utilise la méthode toDouble pour convertir explicitement de ʻInt en Double`.

// Kotlin
val x: Int = 42
val y: Double = x.toDouble() // OK

Notez également que même dans les cas où une conversion explicite est requise en Java, comme Double en ʻInt, Kotlin utilise des méthodes au lieu de la conversion. C'est parce que ʻInt et Double n'ont pas de relation de dérivation de type.

// Java
double x = 42.0;
int y = (int)x; // OK
// Kotlin
val x: Double = 42.0
val y: Int = x as Int // ClassCastException
// Kotlin
val x: Double = 42.0
val y: Int = x.toInt() // OK

Résumé

Dans cet article, j'ai couvert certaines des choses sur lesquelles les programmeurs Java commençant par Kotlin peuvent trouver par hasard parce qu'ils ont besoin de nouvelles idées.

** Article suivant **: Ce que les programmeurs Java trouvent utile avec Kotlin

[^ 1]: Ce n'est pas toujours le code qui garantit qu'il n'est absolument pas «nul». Par exemple, si un utilisateur ne transmet pas d'argument à un script qui doit transmettre un ou plusieurs arguments, il est de la responsabilité de l'utilisateur d'échouer. Le code doit garantir qu'il n'est pas «nul» sur les systèmes critiques, mais dans de nombreux cas, il doit être garanti par l'utilisateur sur des scripts jetables. Dans de tels cas, si vous !! null, vous en êtes responsable.

[^ 2]: Cependant, quand j'essaye d'utiliser ʻObject`, j'obtiens un avertissement du compilateur qu'il ne doit pas être utilisé.

[^ 3]: Java a également une fonction de recherche automatique, mais la fonction de recherche automatique ne convertit automatiquement que des types tels qu'entre ʻint et ʻInteger. Le Kotlin ʻInt` est du même type, qu'il soit encadré en interne ou non, et il n'y a aucune différence dans le code.

[^ 4]: Cependant, puisque ʻIntest un sous-type de ʻInt?, Pour être exact, les types individuels dans" tous les autres Nullables "aux types individuels dans" tous les autres Non-Nulls ". Il y a une ligne qui s'étend à.

[^ 5]: Strictement parlant, en prenant T pour le paramètre de type contravariant du type d'argument, cette expression peut être utilisée à l'envers pour que la multiplication du négatif et du négatif devienne positive, donc cette expression est exacte. Il n'y en a pas. Pour les règles exactes (bien qu'il s'agisse d'un article sur C #) ici S'il te plait regarde.

Recommended Posts

Où les programmeurs Java ont tendance à trébucher sur Kotlin
Classe Kotlin à envoyer aux développeurs Java
[Android] Convertir le code Java Android en Kotlin
Kotlin's Class part.2 à envoyer aux développeurs Java
Convertir toutes les applications Android (Java) en Kotlin
Fonctions de portée Kotlin à envoyer aux développeurs Java
Ce que les programmeurs Java trouvent utile avec Kotlin
Facile à parcourir avec les expressions régulières Java
Mémo pour la migration de Java vers Kotlin
Fonctions Kotlin et lambdas à envoyer aux développeurs Java
Présentation de Kotlin à un projet Java Maven existant
Migrer de Java vers Kotlin côté serveur + Spring-boot
Premiers pas avec Kotlin à envoyer aux développeurs Java
Comment écrire Java String # getBytes dans Kotlin?
Ce que des personnes inexpérimentées en Java ont fait pour étudier Kotlin
Où est Java
[Java] Introduction à Java
Introduction à Java
Maîtriser Kotlin ~ Convertir un fichier Java en fichier Kotlin Road to Graduation ~ Partie 3
Maîtriser Kotlin ~ Convertir un fichier Java en fichier Kotlin Road to Graduation ~ Partie 2
Maîtriser Kotlin ~ Convertir un fichier Java en fichier Kotlin Road to Graduation ~ Partie 1
Je veux faire des transitions d'écran avec kotlin et java!