Laisser plusieurs variables dans Kotlin

127

Existe-t-il un moyen de chaîner plusieurs let pour plusieurs variables nullables dans kotlin?

fun example(first: String?, second: String?) {
    first?.let {
        second?.let {
            // Do something just if both are != null
        }
    }
}

Je veux dire, quelque chose comme ça:

fun example(first: String?, second: String?) {
    first?.let && second?.let { 
        // Do something just if both are != null
    }
}
Daniel Gomez Rico
la source
1
Voulez-vous N articles, pas seulement 2? Tous les éléments ont-ils besoin du même type ou de types différents? Toutes les valeurs doivent-elles être transmises à la fonction, sous forme de liste ou de paramètres individuels? La valeur de retour doit-elle être un élément unique ou un groupe du même nombre d'éléments que l'entrée?
Jayson Minard
J'ai besoin de tous les arguments, peut être deux pour ce cas, mais je voulais aussi savoir un moyen de le faire pour plus, en swift est si facile.
Daniel Gomez Rico le
Cherchez-vous quelque chose de différent des réponses ci-dessous, si oui, commentez quelle est la différence que vous recherchez.
Jayson Minard
Comment serait-ce de faire référence au premier "il" dans le second bloc let?
Javier Mendonça

Réponses:

48

Si vous êtes intéressé, voici deux de mes fonctions pour résoudre ce problème.

inline fun <T: Any> guardLet(vararg elements: T?, closure: () -> Nothing): List<T> {
    return if (elements.all { it != null }) {
        elements.filterNotNull()
    } else {
        closure()
    }
}

inline fun <T: Any> ifLet(vararg elements: T?, closure: (List<T>) -> Unit) {
    if (elements.all { it != null }) {
        closure(elements.filterNotNull())
    }
}

Usage:


// Will print
val (first, second, third) = guardLet("Hello", 3, Thing("Hello")) { return }
println(first)
println(second)
println(third)

// Will return
val (first, second, third) = guardLet("Hello", null, Thing("Hello")) { return }
println(first)
println(second)
println(third)

// Will print
ifLet("Hello", "A", 9) {
 (first, second, third) ->
 println(first)
 println(second)
 println(third)
}

// Won't print
ifLet("Hello", 9, null) {
 (first, second, third) ->
 println(first)
 println(second)
 println(third)
}
Dario Pellegrini
la source
C'est très bien, mais il me manque toujours un cas où je peux utiliser la première entrée dans la seconde. Exemple: ifLet ("A", toLower (first)) {// first = "A", second = "a"}
Otziii
Parce que dans l'instruction ifLet, le premier argument n'a pas encore été déballé, une fonction comme la vôtre n'est pas possible. Puis-je suggérer d'utiliser guardLet? C'est assez simple. val (first) = guardLet (100) {return} val (second) = guardLet (101) {return} val average = average (first, second) Je sais que ce n'est pas ce que vous avez demandé, mais j'espère que cela vous aidera.
Dario Pellegrini
Merci. J'ai plusieurs façons de résoudre ce problème, la raison en est que dans Swift, il est possible d'avoir plusieurs ifLets les uns après les autres séparés par des virgules et ils peuvent utiliser les variables de la vérification précédente. J'aurais aimé que cela soit également possible à Kotlin. :)
Otziii
1
Cela pourrait être une réponse acceptée, mais il y a des frais généraux pour chaque appel. Parce que vm crée d'abord l'objet Function. Compte tenu également de la limitation de dex, cela ajoutera une déclaration de classe Function avec 2 références de méthode pour chaque vérification unique.
Oleksandr Albul
147

Voici quelques variantes, selon le style que vous voudrez utiliser, si vous avez tout de types identiques ou différents, et si la liste d'un nombre inconnu d'éléments ...

Types mixtes, tous ne doivent pas être nuls pour calculer une nouvelle valeur

Pour les types mixtes, vous pouvez créer une série de fonctions pour chaque nombre de paramètres qui peuvent sembler ridicules, mais qui fonctionnent bien pour les types mixtes:

inline fun <T1: Any, T2: Any, R: Any> safeLet(p1: T1?, p2: T2?, block: (T1, T2)->R?): R? {
    return if (p1 != null && p2 != null) block(p1, p2) else null
}
inline fun <T1: Any, T2: Any, T3: Any, R: Any> safeLet(p1: T1?, p2: T2?, p3: T3?, block: (T1, T2, T3)->R?): R? {
    return if (p1 != null && p2 != null && p3 != null) block(p1, p2, p3) else null
}
inline fun <T1: Any, T2: Any, T3: Any, T4: Any, R: Any> safeLet(p1: T1?, p2: T2?, p3: T3?, p4: T4?, block: (T1, T2, T3, T4)->R?): R? {
    return if (p1 != null && p2 != null && p3 != null && p4 != null) block(p1, p2, p3, p4) else null
}
inline fun <T1: Any, T2: Any, T3: Any, T4: Any, T5: Any, R: Any> safeLet(p1: T1?, p2: T2?, p3: T3?, p4: T4?, p5: T5?, block: (T1, T2, T3, T4, T5)->R?): R? {
    return if (p1 != null && p2 != null && p3 != null && p4 != null && p5 != null) block(p1, p2, p3, p4, p5) else null
}
// ...keep going up to the parameter count you care about

Exemple d'utilisation:

val risk = safeLet(person.name, person.age) { name, age ->
  // do something
}   

Exécuter un bloc de code lorsque la liste ne contient aucun élément nul

Deux saveurs ici, d'abord pour exécuter un bloc de code lorsqu'une liste contient tous les éléments non nuls, et deuxièmement pour faire de même lorsqu'une liste a au moins un élément non nul. Les deux cas passent une liste d'éléments non nuls au bloc de code:

Les fonctions:

fun <T: Any, R: Any> Collection<T?>.whenAllNotNull(block: (List<T>)->R) {
    if (this.all { it != null }) {
        block(this.filterNotNull()) // or do unsafe cast to non null collectino
    }
}

fun <T: Any, R: Any> Collection<T?>.whenAnyNotNull(block: (List<T>)->R) {
    if (this.any { it != null }) {
        block(this.filterNotNull())
    }
}

Exemple d'utilisation:

listOf("something", "else", "matters").whenAllNotNull {
    println(it.joinToString(" "))
} // output "something else matters"

listOf("something", null, "matters").whenAllNotNull {
    println(it.joinToString(" "))
} // no output

listOf("something", null, "matters").whenAnyNotNull {
    println(it.joinToString(" "))
} // output "something matters"

Un léger changement pour que la fonction reçoive la liste des éléments et effectue les mêmes opérations:

fun <T: Any, R: Any> whenAllNotNull(vararg options: T?, block: (List<T>)->R) {
    if (options.all { it != null }) {
        block(options.filterNotNull()) // or do unsafe cast to non null collection
    }
}

fun <T: Any, R: Any> whenAnyNotNull(vararg options: T?, block: (List<T>)->R) {
    if (options.any { it != null }) {
        block(options.filterNotNull())
    }
}

Exemple d'utilisation:

whenAllNotNull("something", "else", "matters") {
    println(it.joinToString(" "))
} // output "something else matters"

Ces variations peuvent être modifiées pour avoir des valeurs de retour telles que let().

Utiliser le premier élément non nul (Coalesce)

Similaire à une fonction SQL Coalesce, renvoie le premier élément non nul. Deux saveurs de la fonction:

fun <T: Any> coalesce(vararg options: T?): T? = options.firstOrNull { it != null }
fun <T: Any> Collection<T?>.coalesce(): T? = this.firstOrNull { it != null }

Exemple d'utilisation:

coalesce(null, "something", null, "matters")?.let {
    it.length
} // result is 9, length of "something"

listOf(null, "something", null, "matters").coalesce()?.let {
    it.length
}  // result is 9, length of "something"

Autres variations

... Il existe d'autres variantes, mais avec plus de spécifications, cela pourrait être réduit.

Jayson Minard
la source
1
Vous pouvez également combiner whenAllNotNullavec déstructurant comme ceci: listOf(a, b, c).whenAllNotNull { (d, e, f) -> println("$d $e $f").
dumptruckman
10

Vous pouvez écrire votre propre fonction pour cela:

 fun <T, U, R> Pair<T?, U?>.biLet(body: (T, U) -> R): R? {
     val first = first
     val second = second
     if (first != null && second != null) {
         return body(first, second)
     }
     return null
 }

 (first to second).biLet { first, second -> 
      // body
 }
yole
la source
7

Vous pouvez créer une arrayIfNoNullsfonction:

fun <T : Any> arrayIfNoNulls(vararg elements: T?): Array<T>? {
    if (null in elements) {
        return null
    }
    @Suppress("UNCHECKED_CAST")
    return elements as Array<T>
}

Vous pouvez ensuite l'utiliser pour un nombre variable de valeurs avec let:

fun example(first: String?, second: String?) {
    arrayIfNoNulls(first, second)?.let { (first, second) ->
        // Do something if each element is not null
    }
}

Si vous avez déjà un tableau, vous pouvez créer une takeIfNoNullsfonction (inspirée de takeIfet requireNoNulls):

fun <T : Any> Array<T?>.takeIfNoNulls(): Array<T>? {
    if (null in this) {
        return null
    }
    @Suppress("UNCHECKED_CAST")
    return this as Array<T>
}

Exemple:

array?.takeIfNoNulls()?.let { (first, second) ->
    // Do something if each element is not null
}
mfulton26
la source
3

Pour le cas où il suffit de vérifier deux valeurs et de ne pas avoir à travailler avec des listes:

fun <T1, T2> ifNotNull(value1: T1?, value2: T2?, bothNotNull: (T1, T2) -> (Unit)) {
    if (value1 != null && value2 != null) {
        bothNotNull(value1, value2)
    }
}

Exemple d'utilisation:

var firstString: String?
var secondString: String?
ifNotNull(firstString, secondString) { first, second -> Log.d(TAG, "$first, $second") }
Jonas Hansson
la source
2

En fait, vous pouvez simplement le faire, vous savez? ;)

if (first != null && second != null) {
    // your logic here...
}

Il n'y a rien de mal à utiliser un contrôle nul normal dans Kotlin.

Et c'est beaucoup plus lisible pour tous ceux qui examineront votre code.

Grzegorz D.
la source
36
Ce ne sera pas suffisant lorsqu'il s'agit d'un membre de classe mutable.
Michał K
3
Inutile de donner ce genre de réponse, l'intention de la question est de trouver un moyen plus "productif" de gérer cela, puisque le langage fournit le letraccourci pour faire ces vérifications
Alejandro Moya
1
En termes de maintenabilité, c'est mon choix, même si ce n'est pas aussi élégant. C'est clairement un problème que tout le monde rencontre tout le temps, et la langue devrait traiter.
Brill Pappin le
2

Je préfère en fait le résoudre en utilisant les fonctions d'assistance suivantes:

fun <A, B> T(tuple: Pair<A?, B?>): Pair<A, B>? =
    if(tuple.first == null || tuple.second == null) null
    else Pair(tuple.first!!, tuple.second!!)

fun <A, B, C> T(tuple: Triple<A?, B?, C?>): Triple<A, B, C>? =
    if(tuple.first == null || tuple.second == null || tuple.third == null) null
    else Triple(tuple.first!!, tuple.second!!, tuple.third!!)


fun <A, B> T(first: A?, second: B?): Pair<A, B>? =
    if(first == null || second == null) null
    else Pair(first, second)

fun <A, B, C> T(first: A?, second: B?, third: C?): Triple<A, B, C>? =
        if(first == null || second == null || third == null) null
        else Triple(first, second, third)

Et voici comment vous devez les utiliser:

val a: A? = someValue
val b: B? = someOtherValue
T(a, b)?.let { (a, b) ->
  // Shadowed a and b are of type a: A and b: B
  val c: C? = anotherValue
  T(a, b, c)
}?.let { (a, b, c) ->
  // Shadowed a, b and c are of type a: A, b: B and c: C
  .
  .
  .
}
Moshe Bixenshpaner
la source
1

J'ai résolu ce problème en créant des fonctions qui répliquent plus ou moins le comportement de with, mais prennent plusieurs paramètres et n'appellent que la fonction de tous les paramètres est non nulle.

fun <R, A, B> withNoNulls(p1: A?, p2: B?, function: (p1: A, p2: B) -> R): R? = p1?.let { p2?.let { function.invoke(p1, p2) } }
fun <R, A, B, C> withNoNulls(p1: A?, p2: B?, p3: C?, function: (p1: A, p2: B, p3: C) -> R): R? = p1?.let { p2?.let { p3?.let { function.invoke(p1, p2, p3) } } }
fun <R, A, B, C, D> withNoNulls(p1: A?, p2: B?, p3: C?, p4: D?, function: (p1: A, p2: B, p3: C, p4: D) -> R): R? = p1?.let { p2?.let { p3?.let { p4?.let { function.invoke(p1, p2, p3, p4) } } } }
fun <R, A, B, C, D, E> withNoNulls(p1: A?, p2: B?, p3: C?, p4: D?, p5: E?, function: (p1: A, p2: B, p3: C, p4: D, p5: E) -> R): R? = p1?.let { p2?.let { p3?.let { p4?.let { p5?.let { function.invoke(p1, p2, p3, p4, p5) } } } } }

Ensuite, je l'utilise comme ceci:

withNoNulls("hello", "world", Throwable("error")) { p1, p2, p3 ->
    p3.printStackTrace()
    p1.plus(" ").plus(p2)
}?.let {
    Log.d("TAG", it)
} ?: throw Exception("One or more parameters was null")

Le problème évident avec ceci est que je dois définir une fonction pour chaque cas (nombre de variables) dont j'ai besoin, mais au moins je pense que le code semble propre lors de leur utilisation.

Jon
la source
1

Tu pourrais aussi faire ça

if (listOfNotNull(var1, var2, var3).size == 3) {
        // All variables are non-null
}
Amy Eubanks
la source
Le compilateur se plaindra toujours de ne pas pouvoir garantir que les vars ne sont pas nulles
Peter Graham
1

J'ai légèrement amélioré la réponse attendue:

inline fun <T: Any, R: Any> ifLet(vararg elements: T?, closure: (List<T>) -> R): R? {
    return if (elements.all { it != null }) {
        closure(elements.filterNotNull())
    } else null
}

cela rend cela possible:

iflet("first", "sconed") {
    // do somehing
} ?: run {
    // do this if one of the params are null
}
yohai knaani
la source
C'est cool, mais les paramètres ne sont pas nommés et devraient partager le type.
Daniel Gomez Rico le
0

Pour toute quantité de valeurs à vérifier, vous pouvez utiliser ceci:

    fun checkNulls(vararg elements: Any?, block: (Array<*>) -> Unit) {
        elements.forEach { if (it == null) return }
        block(elements.requireNoNulls())
    }

Et il sera utilisé comme ceci:

    val dada: String? = null
    val dede = "1"

    checkNulls(dada, dede) { strings ->

    }

les éléments envoyés au bloc utilisent le caractère générique, vous devez vérifier les types si vous souhaitez accéder aux valeurs, si vous avez besoin d'utiliser un seul type, vous pouvez le transformer en génériques

Alejandro Moya
la source