Множественная переменная let в Котлине

127

Есть ли способ связать несколько let для нескольких переменных, допускающих значение NULL, в kotlin?

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

Я имею в виду, что-то вроде этого:

fun example(first: String?, second: String?) {
    first?.let && second?.let { 
        // Do something just if both are != null
    }
}
Даниэль Гомес Рико
источник
1
Вы хотите N элементов, а не только 2? Все ли предметы нужны одного или разных типов? Все значения должны передаваться в функцию в виде списка или отдельных параметров? Должно ли возвращаемое значение быть одним элементом или группой из того же количества элементов, что и вход?
Джейсон Минард,
Мне нужны все аргументы, может быть два для этого случая, но я также хотел узнать, как сделать это для большего, быстро это так просто.
Даниэль Гомес Рико
Вы ищете что-то иное, чем ответы ниже, если да, то прокомментируйте, в чем разница, которую вы ищете.
Джейсон Минард,
Как было бы ссылаться на первое «оно» во втором блоке let?
Хавьер Мендонса

Ответы:

49

Если интересно, вот две мои функции для решения этой проблемы.

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

Использование:


// 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)
}
Дарио Пеллегрини
источник
Это очень хорошо, но мне все еще не хватает случая, когда я могу использовать первый вход во втором. Пример: ifLet ("A", toLower (first)) {// first = "A", second = "a"}
Otziii
Причина в том, что в операторе ifLet первый аргумент еще не развернут, функция, подобная вашей, невозможна. Могу я предложить использовать guardLet? Это довольно просто. val (first) = guardLet (100) {return} val (second) = guardLet (101) {return} val average = average (first, second) Я знаю, что это не то, о чем вы спрашивали, но надеюсь, что это поможет.
Дарио Пеллегрини
Спасибо. У меня есть несколько способов решить эту проблему, причина в том, что в Swift можно иметь несколько ifLet после друг друга, разделенных запятой, и они могут использовать переменные предыдущей проверки. Я бы хотел, чтобы это было возможно и в Котлине. :)
Otziii 02
1
Это может быть принятый ответ, но при каждом вызове есть накладные расходы. Поскольку vm сначала создает объект Function. Также с учетом ограничения dex, это добавит объявление класса функции с двумя ссылками на методы для каждой уникальной проверки.
Александр Албул
147

Вот несколько вариантов, в зависимости от того, какой стиль вы хотите использовать, если у вас все одного или разных типов, и если в списке неизвестное количество элементов ...

Смешанные типы, все не должны быть нулевыми, чтобы вычислить новое значение

Для смешанных типов вы можете создать серию функций для каждого количества параметров, которые могут выглядеть глупо, но хорошо работают для смешанных типов:

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

Пример использования:

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

Выполнить блок кода, если в списке нет нулевых элементов

Здесь два варианта: первый для выполнения блока кода, когда в списке есть все ненулевые элементы, и второй, чтобы делать то же самое, когда в списке есть хотя бы один ненулевой элемент. В обоих случаях в блок кода передается список ненулевых элементов:

Функции:

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

Пример использования:

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"

Небольшое изменение, чтобы функция получала список элементов и выполняла те же операции:

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

Пример использования:

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

Эти варианты можно изменить, чтобы они возвращали такие значения, как let().

Использовать первый ненулевой элемент (Coalesce)

Подобно функции SQL Coalesce, возвращает первый ненулевой элемент. Две разновидности функции:

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

Пример использования:

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"

Другие варианты

... Есть и другие варианты, но с большей спецификацией их можно было бы сузить.

Джейсон Минард
источник
1
Кроме того, можно комбинировать whenAllNotNullс таким , как уничтожение того : listOf(a, b, c).whenAllNotNull { (d, e, f) -> println("$d $e $f").
dumptruckman
10

Вы можете написать для этого свою функцию:

 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
источник
7

Вы можете создать arrayIfNoNullsфункцию:

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

Затем вы можете использовать его для переменного количества значений с помощью let:

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

Если у вас уже есть массив, вы можете создать takeIfNoNullsфункцию (вдохновленную takeIfи requireNoNulls):

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

Пример:

array?.takeIfNoNulls()?.let { (first, second) ->
    // Do something if each element is not null
}
mfulton26
источник
3

В случае простой проверки двух значений и отсутствия работы со списками:

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

Пример использования:

var firstString: String?
var secondString: String?
ifNotNull(firstString, secondString) { first, second -> Log.d(TAG, "$first, $second") }
Йонас Ханссон
источник
2

На самом деле, вы можете просто сделать это, понимаете? ;)

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

Нет ничего плохого в использовании обычной проверки на нуль в Kotlin.

И он намного удобнее для чтения для всех, кто будет изучать ваш код.

Гжегож Д.
источник
36
Этого будет недостаточно при работе с изменяемым членом класса.
Michał K
3
Нет необходимости давать такой ответ, цель вопроса состоит в том, чтобы найти более «продуктивный способ» справиться с этим, поскольку язык предоставляет letярлык для выполнения этих проверок
Алехандро Мойя
1
С точки зрения ремонтопригодности это мой выбор, пусть даже не такой элегантный. Очевидно, что это проблема, с которой все сталкиваются постоянно, и язык должен решать ее.
Брилл Паппин,
2

На самом деле я предпочитаю решать эту проблему с помощью следующих вспомогательных функций:

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)

И вот как их следует использовать:

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
  .
  .
  .
}
Моше Биксеншпанер
источник
1

Я решил эту проблему, создав несколько функций, которые более или менее повторяют поведение with, но принимают несколько параметров и вызывают только функцию, все параметры которой не равны нулю.

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

Тогда я использую это так:

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

Очевидная проблема заключается в том, что мне нужно определять функцию для каждого случая (количества переменных), которые мне нужны, но, по крайней мере, я думаю, что код выглядит чистым при их использовании.

Джон
источник
1

Вы также можете сделать это

if (listOfNotNull(var1, var2, var3).size == 3) {
        // All variables are non-null
}
Эми Юбэнкс
источник
Компилятор все равно будет жаловаться, что не может гарантировать, что переменные не равны нулю
Питер Грэм,
1

Я немного обновил ожидаемый ответ:

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
}

это делает это возможным:

iflet("first", "sconed") {
    // do somehing
} ?: run {
    // do this if one of the params are null
}
Йохай Кнаани
источник
Это круто, но параметры не имеют названий и должны иметь одинаковый тип.
Даниэль Гомес Рико,
0

Для любого количества проверяемых значений вы можете использовать это:

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

И он будет использоваться так:

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

    checkNulls(dada, dede) { strings ->

    }

элементы, отправленные в блок, используют подстановочный знак, вам необходимо проверить типы, если вы хотите получить доступ к значениям, если вам нужно использовать только один тип, вы можете изменить его на общие

Алехандро Мойя
источник