Dans Kotlin, quelle est la manière idiomatique de traiter les valeurs Nullable, de les référencer ou de les convertir

177

Si j'ai un type Nullable Xyz?, je veux le référencer ou le convertir en un type non Nullable Xyz. Quelle est la manière idiomatique de le faire à Kotlin?

Par exemple, ce code est en erreur:

val something: Xyz? = createPossiblyNullXyz()
something.foo() // Error: "Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Xyz?"

Mais si je vérifie d'abord null, c'est autorisé, pourquoi?

val something: Xyz? = createPossiblyNullXyz()
if (something != null) {
    something.foo() 
}

Comment puis-je modifier ou traiter une valeur comme non nullsans exiger la ifvérification, en supposant que je sache avec certitude que ce n'est vraiment jamais null? Par exemple, ici, je récupère une valeur à partir d'une carte dont je peux garantir l'existence et que le résultat get()ne l'est pas null. Mais j'ai une erreur:

val map = mapOf("a" to 65,"b" to 66,"c" to 67)
val something = map.get("a")
something.toLong() // Error: "Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Int?"

La méthode get()pense qu'il est possible que l'élément soit manquant et renvoie le type Int?. Par conséquent, quelle est la meilleure façon de forcer le type de la valeur à ne pas être Nullable?

Remarque: cette question est intentionnellement écrite et répondue par l'auteur (questions auto-répondues ), de sorte que les réponses idiomatiques aux sujets Kotlin fréquemment posées soient présentes dans SO. Aussi pour clarifier certaines réponses vraiment anciennes écrites pour les alphas de Kotlin qui ne sont pas exactes pour Kotlin d'aujourd'hui.

Jayson Minard
la source

Réponses:

296

Tout d'abord, vous devriez tout lire sur Null Safety à Kotlin qui couvre les cas à fond.

Dans Kotlin, vous ne pouvez pas accéder à une valeur nullable sans être sûr que ce n'est pas le cas null( Vérifier la valeur nulle dans les conditions ), ou affirmer qu'il nulln'utilise sûrement pas l' !!opérateur sure , y accéder avec un ?.appel sécurisé , ou enfin donner quelque chose qui est peut-être nullun valeur par défaut à l'aide de l' ?:opérateur Elvis .

Pour votre premier cas dans votre question, vous avez des options en fonction de l'intention du code que vous utiliseriez l'une d'entre elles, et toutes sont idiomatiques mais ont des résultats différents:

val something: Xyz? = createPossiblyNullXyz()

// access it as non-null asserting that with a sure call
val result1 = something!!.foo()

// access it only if it is not null using safe operator, 
// returning null otherwise
val result2 = something?.foo()

// access it only if it is not null using safe operator, 
// otherwise a default value using the elvis operator
val result3 = something?.foo() ?: differentValue

// null check it with `if` expression and then use the value, 
// similar to result3 but for more complex cases harder to do in one expression
val result4 = if (something != null) {
                   something.foo() 
              } else { 
                   ...
                   differentValue 
              }

// null check it with `if` statement doing a different action
if (something != null) { 
    something.foo() 
} else { 
    someOtherAction() 
}

Pour le "Pourquoi cela fonctionne-t-il lorsque nul est coché", lisez les informations générales ci-dessous sur les casts intelligents .

Pour votre deuxième cas dans votre question dans la question avec Map, si vous en tant que développeur êtes sûr que le résultat ne sera jamais null, utilisez l' !!opérateur sure comme assertion:

val map = mapOf("a" to 65,"b" to 66,"c" to 67)
val something = map.get("a")!!
something.toLong() // now valid

ou dans un autre cas, lorsque la carte POURRAIT retourner un null mais que vous pouvez fournir une valeur par défaut, alors Mapelle-même a une getOrElseméthode :

val map = mapOf("a" to 65,"b" to 66,"c" to 67)
val something = map.getOrElse("z") { 0 } // provide default value in lambda
something.toLong() // now valid

Informations d'arrière-plan:

Remarque: dans les exemples ci-dessous, j'utilise des types explicites pour clarifier le comportement. Avec l'inférence de type, les types peuvent normalement être omis pour les variables locales et les membres privés.

En savoir plus sur l' !!opérateur sûr

L' !!opérateur affirme que la valeur n'est pas nullou lève un NPE. Cela doit être utilisé dans les cas où le développeur garantit que la valeur ne sera jamais null. Pensez-y comme une affirmation suivie d'un casting intelligent .

val possibleXyz: Xyz? = ...
// assert it is not null, but if it is throw an exception:
val surelyXyz: Xyz = possibleXyz!! 
// same thing but access members after the assertion is made:
possibleXyz!!.foo()

en savoir plus: !! Opérateur sûr


En savoir plus sur la nullvérification et les casts intelligents

Si vous protégez l'accès à un type Nullable avec une nullvérification, le compilateur convertira intelligemment la valeur dans le corps de l'instruction pour qu'elle ne puisse pas Nullable. Il existe des flux complexes où cela ne peut pas se produire, mais pour les cas courants, cela fonctionne bien.

val possibleXyz: Xyz? = ...
if (possibleXyz != null) {
   // allowed to reference members:
   possiblyXyz.foo()
   // or also assign as non-nullable type:
   val surelyXyz: Xyz = possibleXyz
}

Ou si vous effectuez une isvérification pour un type non Nullable:

if (possibleXyz is Xyz) {
   // allowed to reference members:
   possiblyXyz.foo()
}

Et la même chose pour les expressions `` quand '' qui sont également diffusées en toute sécurité:

when (possibleXyz) {
    null -> doSomething()
    else -> possibleXyz.foo()
}

// or

when (possibleXyz) {
    is Xyz -> possibleXyz.foo()
    is Alpha -> possibleXyz.dominate()
    is Fish -> possibleXyz.swim() 
}

Certaines choses ne permettent pas à la nullvérification d'effectuer un cast intelligent pour une utilisation ultérieure de la variable. L'exemple ci - dessus utilise une variable locale qui pouvait en aucune façon avoir muté dans le flux de l'application, que ce soit valou varcette variable a eu aucune possibilité de muter en une null. Mais, dans d'autres cas où le compilateur ne peut pas garantir l'analyse de flux, ce serait une erreur:

var nullableInt: Int? = ...

public fun foo() {
    if (nullableInt != null) {
        // Error: "Smart cast to 'kotlin.Int' is impossible, because 'nullableInt' is a mutable property that could have been changed by this time"
        val nonNullableInt: Int = nullableInt
    }
}

Le cycle de vie de la variable nullableIntn'est pas complètement visible et peut être attribué à partir d'autres threads, la nullvérification ne peut pas être convertie de manière intelligente en une valeur non nullable. Consultez la rubrique «Appels sécurisés» ci-dessous pour une solution de contournement.

Un autre cas qui ne peut pas être approuvé par un cast intelligent pour ne pas muter est une valpropriété sur un objet qui a un getter personnalisé. Dans ce cas, le compilateur n'a aucune visibilité sur ce qui mute la valeur et vous obtiendrez donc un message d'erreur:

class MyThing {
    val possibleXyz: Xyz? 
        get() { ... }
}

// now when referencing this class...

val thing = MyThing()
if (thing.possibleXyz != null) {
   // error: "Kotlin: Smart cast to 'kotlin.Int' is impossible, because 'p.x' is a property that has open or custom getter"
   thing.possiblyXyz.foo()
}

en savoir plus: Vérification de la valeur nulle dans les conditions


En savoir plus sur l' ?.opérateur Safe Call

L'opérateur d'appel sécurisé renvoie null si la valeur à gauche est nulle, sinon continue d'évaluer l'expression à droite.

val possibleXyz: Xyz? = makeMeSomethingButMaybeNullable()
// "answer" will be null if any step of the chain is null
val answer = possibleXyz?.foo()?.goo()?.boo()

Un autre exemple où vous souhaitez parcourir une liste mais seulement si ce n'est pas le cas nullet non vide, encore une fois, l'opérateur d'appel sécurisé est pratique:

val things: List? = makeMeAListOrDont()
things?.forEach {
    // this loops only if not null (due to safe call) nor empty (0 items loop 0 times):
}

Dans l'un des exemples ci-dessus, nous avons eu un cas où nous avons fait une ifvérification mais avons la chance qu'un autre thread ait muté la valeur et donc pas de conversion intelligente . Nous pouvons modifier cet exemple pour utiliser l'opérateur d'appel sécurisé avec la letfonction pour résoudre ce problème:

var possibleXyz: Xyz? = 1

public fun foo() {
    possibleXyz?.let { value ->
        // only called if not null, and the value is captured by the lambda
        val surelyXyz: Xyz = value
    }
}

en savoir plus: Appels sécurisés


En savoir plus sur l' ?:opérateur Elvis

L'opérateur Elvis vous permet de fournir une valeur alternative lorsqu'une expression à gauche de l'opérateur est null:

val surelyXyz: Xyz = makeXyzOrNull() ?: DefaultXyz()

Il a également quelques utilisations créatives, par exemple, lever une exception lorsque quelque chose est null:

val currentUser = session.user ?: throw Http401Error("Unauthorized")

ou pour revenir tôt d'une fonction:

fun foo(key: String): Int {
   val startingCode: String = codes.findKey(key) ?: return 0
   // ...
   return endingValue
}

En savoir plus: Elvis Operator


Opérateurs Null avec des fonctions associées

Kotlin stdlib a une série de fonctions qui fonctionnent très bien avec les opérateurs mentionnés ci-dessus. Par exemple:

// use ?.let() to change a not null value, and ?: to provide a default
val something = possibleNull?.let { it.transform() } ?: defaultSomething

// use ?.apply() to operate further on a value that is not null
possibleNull?.apply {
    func1()
    func2()
}

// use .takeIf or .takeUnless to turn a value null if it meets a predicate
val something = name.takeIf { it.isNotBlank() } ?: defaultName

val something = name.takeUnless { it.isBlank() } ?: defaultName

Rubriques connexes

Dans Kotlin, la plupart des applications essaient d'éviter les nullvaleurs, mais ce n'est pas toujours possible. Et cela a parfois nullun sens parfait. Quelques lignes directrices à prendre en compte:

  • dans certains cas, il garantit différents types de retour qui incluent l'état de l'appel de méthode et le résultat en cas de succès. Les bibliothèques telles que Result vous donnent un type de résultat de réussite ou d'échec qui peut également créer une branche de votre code. Et la bibliothèque Promises pour Kotlin appelée Kovenant fait de même sous forme de promesses.

  • pour les collections, les types de retour renvoient toujours une collection vide au lieu de a null, sauf si vous avez besoin d'un troisième état «non présent». Kotlin a des fonctions d'assistance telles que emptyList()ouemptySet() pour créer ces valeurs vides.

  • lorsque vous utilisez des méthodes qui renvoient une valeur Nullable pour laquelle vous avez une valeur par défaut ou une alternative, utilisez l'opérateur Elvis pour fournir une valeur par défaut. Dans le cas d'une Maputilisation du getOrElse()qui permet de générer une valeur par défaut à la place d'une Mapméthode get()qui renvoie une valeur Nullable. Pareil pourgetOrPut()

  • lors de la substitution de méthodes à partir de Java où Kotlin n'est pas sûr de la nullité du code Java, vous pouvez toujours supprimer la ?nullabilité de votre remplacement si vous êtes sûr de la signature et de la fonctionnalité. Par conséquent, votre méthode remplacée est plus nullsûre. Idem pour l'implémentation d'interfaces Java dans Kotlin, modifiez la nullité pour qu'elle soit ce que vous savez être valide.

  • regardez les fonctions qui peuvent déjà aider, comme for String?.isNullOrEmpty()et String?.isNullOrBlank()qui peuvent fonctionner sur une valeur Nullable en toute sécurité et faites ce que vous attendez. En fait, vous pouvez ajouter vos propres extensions pour combler les lacunes de la bibliothèque standard.

  • assertion fonctionne comme checkNotNull()et requireNotNull()dans la bibliothèque standard.

  • des fonctions d'assistance comme filterNotNull()qui suppriment les valeurs nulles des collections, ou listOfNotNull()pour renvoyer une liste d'éléments zéro ou unique à partir d'une nullvaleur éventuelle .

  • il existe également un opérateur de conversion Safe (nullable) qui permet à un cast en type non nullable de retourner null si ce n'est pas possible. Mais je n'ai pas de cas d'utilisation valide pour cela qui n'est pas résolu par les autres méthodes mentionnées ci-dessus.

Jayson Minard
la source
1

La réponse précédente est un acte difficile à suivre, mais voici un moyen simple et rapide:

val something: Xyz = createPossiblyNullXyz() ?: throw RuntimeError("no it shouldn't be null")
something.foo() 

S'il n'est vraiment jamais nul, l'exception ne se produira pas, mais si c'est le cas, vous verrez ce qui n'a pas fonctionné.

John Farrell
la source
12
val quelque chose: Xyz = createPossiblementNullXyz () !! lancera un NPE lorsque createPossiblementNullXyz () retourne null. C'est plus simple et suit les conventions pour traiter une valeur dont vous savez qu'elle n'est pas nulle
Steven Waterman