Mehrere Variablen lassen Kotlin ein

127

Gibt es eine Möglichkeit, mehrere Lets für mehrere nullfähige Variablen in Kotlin zu verketten?

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

Ich meine, so etwas:

fun example(first: String?, second: String?) {
    first?.let && second?.let { 
        // Do something just if both are != null
    }
}
Daniel Gomez Rico
quelle
1
Möchten Sie N Artikel, nicht nur 2? Benötigen alle Artikel den gleichen Typ oder unterschiedliche Typen? Sollten alle Werte als Liste oder als einzelne Parameter an die Funktion übergeben werden? Sollte der Rückgabewert ein einzelnes Element oder eine Gruppe mit der gleichen Anzahl von Elementen wie die Eingabe sein?
Jayson Minard
Ich brauche alle Argumente, kann für diesen Fall zwei sein, wollte aber auch einen Weg finden, dies für mehr zu tun, schnell ist das so einfach.
Daniel Gomez Rico
Suchen Sie etwas anderes als die folgenden Antworten? Wenn ja, kommentieren Sie, welchen Unterschied Sie suchen.
Jayson Minard
Wie wäre es, sich auf das erste "es" innerhalb des zweiten let-Blocks zu beziehen?
Javier Mendonça

Antworten:

48

Bei Interesse sind hier zwei meiner Funktionen zur Lösung dieses Problems.

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())
    }
}

Verwendung:


// 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
quelle
Das ist sehr schön, aber mir fehlt immer noch ein Fall, in dem ich den ersten Eingang im zweiten verwenden kann. Beispiel: ifLet ("A", toLower (first)) {// first = "A", second = "a"}
Otziii
Da in der ifLet-Anweisung das erste Argument noch nicht entpackt wurde, ist eine Funktion wie Ihre nicht möglich. Kann ich vorschlagen, guardLet zu verwenden? Es ist ziemlich einfach. val (first) = guardLet (100) {return} val (second) = guardLet (101) {return} val durchschnitt = durchschnittlich (first, second) Ich weiß, dass Sie nicht danach gefragt haben, aber ich hoffe, es hilft.
Dario Pellegrini
Vielen Dank. Ich habe mehrere Möglichkeiten, dies zu lösen. Der Grund dafür ist, dass es in Swift möglich ist, mehrere ifLets nacheinander durch Komma getrennt zu haben und die Variablen der vorherigen Prüfung zu verwenden. Ich wünschte, das wäre auch in Kotlin möglich. :)
Otziii
1
Es könnte eine akzeptierte Antwort sein, aber bei jedem Anruf entsteht ein Overhead. Weil vm zuerst ein Funktionsobjekt erstellt. Unter Berücksichtigung der Dex-Einschränkung wird dadurch eine Funktionsklassendeklaration mit 2 Methodenreferenzen für jede eindeutige Prüfung hinzugefügt.
Oleksandr Albul
146

Hier sind einige Variationen, je nachdem, welchen Stil Sie verwenden möchten, ob Sie alle gleichen oder unterschiedlichen Typen haben und ob die Liste eine unbekannte Anzahl von Elementen enthält ...

Gemischte Typen, alle dürfen nicht null sein, um einen neuen Wert zu berechnen

Für gemischte Typen können Sie eine Reihe von Funktionen für jede Parameteranzahl erstellen, die zwar albern aussehen, für gemischte Typen jedoch gut funktionieren:

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

Anwendungsbeispiel:

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

Führen Sie einen Codeblock aus, wenn die Liste keine Nullelemente enthält

Hier zwei Varianten: erstens das Ausführen eines Codeblocks, wenn eine Liste alle Nicht-Null-Elemente enthält, und zweitens, wenn eine Liste mindestens ein Nicht-Null-Element enthält. In beiden Fällen wird eine Liste von Nicht-Null-Elementen an den Codeblock übergeben:

Funktionen:

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())
    }
}

Anwendungsbeispiel:

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"

Eine geringfügige Änderung, damit die Funktion die Liste der Elemente empfängt und dieselben Vorgänge ausführt:

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())
    }
}

Anwendungsbeispiel:

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

Diese Variationen könnten geändert werden, um Rückgabewerte wie zu haben let().

Verwenden Sie das erste Nicht-Null-Element (Coalesce).

Geben Sie ähnlich wie bei einer SQL Coalesce-Funktion das erste Nicht-Null-Element zurück. Zwei Varianten der Funktion:

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

Anwendungsbeispiel:

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"

Andere Variationen

... Es gibt andere Variationen, aber mit einer genaueren Spezifikation könnte dies eingegrenzt werden.

Jayson Minard
quelle
1
Sie können auch mit einer solchen whenAllNotNullDestrukturierung kombinieren : listOf(a, b, c).whenAllNotNull { (d, e, f) -> println("$d $e $f").
Dumptruckman
10

Sie können dafür Ihre eigene Funktion schreiben:

 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
quelle
7

Sie können eine arrayIfNoNullsFunktion erstellen :

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

Sie können es dann für eine variable Anzahl von Werten verwenden mit let:

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

Wenn Sie bereits ein Array haben, können Sie eine takeIfNoNullsFunktion erstellen (inspiriert von takeIfund requireNoNulls):

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

Beispiel:

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

Für den Fall, nur zwei Werte zu überprüfen und auch nicht mit Listen arbeiten zu müssen:

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

Anwendungsbeispiel:

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

Eigentlich kannst du das einfach machen, weißt du? ;)

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

Es ist nichts Falsches daran, einen normalen Null-Check in Kotlin zu verwenden.

Und es ist weitaus besser lesbar für alle, die sich Ihren Code ansehen.

Grzegorz D.
quelle
36
Es wird nicht genug sein, wenn es um ein veränderliches Klassenmitglied geht.
Michał K
3
Keine Notwendigkeit, diese Art von Antwort zu geben, die Absicht der Frage ist es, einen "produktiveren Weg" zu finden, um damit umzugehen, da die Sprache die letAbkürzung für diese Überprüfungen bietet
Alejandro Moya
1
In Bezug auf die Wartbarkeit ist dies meine Wahl, auch wenn es nicht so elegant ist. Dies ist eindeutig ein Problem, auf das jeder ständig stößt, und die Sprache sollte sich damit befassen.
Brill Pappin
2

Ich ziehe es eigentlich vor, es mit den folgenden Hilfsfunktionen zu lösen:

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)

Und so sollten Sie sie verwenden:

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
quelle
1

Ich habe dieses Problem gelöst, indem ich einige Funktionen erstellt habe, die das Verhalten von with mehr oder weniger replizieren, aber mehrere Parameter verwenden und nur die Funktion aller Parameter aufrufen, die nicht null ist.

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) } } } } }

Dann benutze ich es so:

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")

Das offensichtliche Problem dabei ist, dass ich für jeden Fall (Anzahl der Variablen), die ich benötige, eine Funktion definieren muss, aber zumindest denke ich, dass der Code sauber aussieht, wenn ich sie verwende.

Jon
quelle
1

Sie könnten dies auch tun

if (listOfNotNull(var1, var2, var3).size == 3) {
        // All variables are non-null
}
Amy Eubanks
quelle
Der Compiler wird sich weiterhin beschweren, dass er nicht garantieren kann, dass die Vars nicht null sind
Peter Graham
1

Ich habe die erwartete Antwort ein wenig aktualisiert:

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
}

das macht dies möglich:

iflet("first", "sconed") {
    // do somehing
} ?: run {
    // do this if one of the params are null
}
Yohai Knaani
quelle
Das ist cool, aber die Parameter sind nicht benannt und sollten den Typ teilen.
Daniel Gomez Rico
0

Für eine beliebige Anzahl von zu überprüfenden Werten können Sie Folgendes verwenden:

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

Und es wird so verwendet:

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

    checkNulls(dada, dede) { strings ->

    }

Die an den Block gesendeten Elemente verwenden den Platzhalter. Sie müssen die Typen überprüfen, wenn Sie auf die Werte zugreifen möchten. Wenn Sie nur einen Typ verwenden müssen, können Sie dies zu Generika mutieren

Alejandro Moya
quelle