【问题标题】:Multiple variable let in KotlinKotlin 中的多变量 let
【发布时间】:2016-06-01 12:25:51
【问题描述】:

有没有办法在 kotlin 中为多个可为空的变量链接多个 let?

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

【问题讨论】:

  • 您想要 N 项,而不仅仅是 2 项吗?所有项目都需要相同类型还是不同类型?应该将所有值作为列表还是作为单个参数传递给函数?返回值应该是单个项目还是一组与输入相同数量的项目?
  • 我需要所有参数,在这种情况下可以是两个,但也想知道一种方法来做更多的事情,在 swift 中很容易。
  • 您是否正在寻找与以下答案不同的东西,如果是,请评论您正在寻找的差异是什么。
  • 如何在第二个 let 块中引用第一个“it”?

标签: kotlin


【解决方案1】:

这里有一些变化,具体取决于您要使用的样式、您是否拥有相同或不同类型的所有内容以及列表中的项目数量是否未知...

混合类型,都不能为null计算新值

对于混合类型,您可以为每个参数计数构建一系列函数,这些函数可能看起来很傻,但对于混合类型来说效果很好:

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() 的返回值。

使用第一个非空项(合并)

类似于 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"

其他变体

...还有其他变体,但如果有更多规范,这可能会缩小范围。

【讨论】:

  • 您还可以将whenAllNotNull 与解构结合起来,如下所示:listOf(a, b, c).whenAllNotNull { (d, e, f) -&gt; println("$d $e $f")
  • "..继续增加你关心的参数计数"我会说这不是方式
  • Arrow.kt 有第一个替代方案的实现:arrow-kt.io/docs/apidocs/arrow-core/arrow.core/-nullable/…
【解决方案2】:

如果有兴趣,我有两个函数可以解决这个问题。

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" }
  • 因为 ifLet 语句中的第一个参数还没有被解包,像你这样的函数是不可能的。我可以建议使用guardLet吗?这很简单。 val (first) = guardLet(100) { return } val (second) = guardLet(101) { return } val average = average(first, second) 我知道这不是您要问的,但希望对您有所帮助。
  • 它可以被接受,但每次通话都会产生开销。因为 vm 首先创建了 Function 对象。还考虑到 dex 限制,这将为每个唯一检查添加带有 2 个方法引用的 Function 类声明。
  • @OleksandrAlbul 这不正确。 vm 不会创建函数对象,因为函数及其传入的所有 lambda 都是内联的。因此,它们的内部代码在编译时被复制到使用函数的任何地方。唯一的开销来自创建集合的可变参数。
  • 也许我错了,但看起来例如当你将不同的类型传递给这些函数时,T 将被推断为 any。因此,lambda 变量 first、second、third 是 Any 类型,这意味着您需要将它们强制转换为对它们做任何有用的事情。
【解决方案3】:

您可以为此编写自己的函数:

 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
 }

【讨论】:

  • 如果有人要检查 4 个、另外 6 个和另外 8 个参数怎么办?!只是继续添加那些“花哨”的论点?! ))
【解决方案4】:

我喜欢使用列表过滤空值的想法,当我使用相同类型时,我通常会做类似的事情,但是当有多种类型时,为了避免将值解析为Any,我只是做这样的事情

fun someFunction() {
    val value1: String = this.value1 ?: return
    val value2: Int = this.value2 ?: return
    ...
 }

它有效,对我来说保持类型安全很重要

【讨论】:

    【解决方案5】:

    您可以创建一个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 函数(灵感来自takeIfrequireNoNulls):

    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
    }
    

    【讨论】:

    • 如果通过数组传递 first 和 second 的类型,如何保留它们?
    • 你是对的。这是这个解决方案的一个缺点。
    【解决方案6】:

    实际上,你可以简单地做到这一点,你知道吗? ;)

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

    在 Kotlin 中使用普通的空检查没有任何问题。

    而且对于所有将要查看您的代码的人来说,它的可读性要高得多。

    【讨论】:

    • 在处理可变类成员时是不够的。
    • 不需要给出这种答案,问题的目的是找到一种更“有效的方式”来处理这个问题,因为语言提供了let 快捷方式来进行这些检查跨度>
    • 在可维护性方面,这是我的选择,即使它不那么优雅。这显然是每个人都会遇到的问题,语言应该处理。
    • 鉴于函数参数是不可变的(根据问题),使用 let 并不理想,因为它引入了不必要的临时变量,因此直接检查 null 是直接且更有效的 @Gzegorz 共享. medium.com/mobile-app-development-publication/… 为答案投票。
    • @OkhanOkbay 一个可变的类成员(在类主体中声明的var)可以是例如通过不同的方法异步更改。即使您检查了 first 变量是否为空,在下一行它可能再次为空,因为它可能已经被更改。 Kotlin 编译器知道这一点,并且在下一行中它仍然是一个可为空的 var。在这方面使用first?.let { it.foo() } 更安全。然而,这不是一个日常问题,有时常规 nullcheck 的表现力很好(正如 Grzegorz D. 所说)。
    【解决方案7】:

    对于只检查两个值而不必使用列表的情况:

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

    【讨论】:

      【解决方案8】:

      我实际上更喜欢使用以下辅助函数来解决它:

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

      【讨论】:

        【解决方案9】:

        我已经将预期的答案升级了一点:

        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
        }
        

        【讨论】:

        • 这很酷,但是参数没有命名并且应该共享类型。
        【解决方案10】:

        我通过创建一些函数来解决这个问题,这些函数或多或少复制了 with 的行为,但需要多个参数并且只调用所有参数为非 null 的函数。

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

        这样做的明显问题是我必须为我需要的每种情况(变量数量)定义一个函数,但至少我认为使用它们时代码看起来很干净。

        【讨论】:

          【解决方案11】:

          你也可以这样做

          if (listOfNotNull(var1, var2, var3).size == 3) {
                  // All variables are non-null
          }
          

          【讨论】:

          • 编译器还是会抱怨不能保证vars不为空
          • 这是一个涉及可维护性的情况。如果您计划对代码进行更改,则需要确保您在未来所做的任何更改都不会在您的 null 检查后将该 var 变为 null。它甚至可以停止您认为变量未更改但已更改的情况(例如在您的空检查后更改 var 的后台线程)。
          【解决方案12】:

          基于@yole 回答的另一个想法

          fun <T, U, R> Pair<T?, U?>.toLet(body: (List<*>) -> R): R? {
              val one = first
              val two = second
              if (one == null || two == null)
                  return null
              return if (one is Pair<*, *>) {
                  one.toLet { a ->
                      body(listOf(a, listOf(two)).flatten())
                  }
              } else {
                  body(listOf(one, two))
              }
          }
          

          所以您可以执行以下操作

          (1 to 6 to "a" to 4.5).toLet { (a, b, c, d) ->
              // Rest of code
          }
          
          

          【讨论】:

            【解决方案13】:

            也许有点晚了。但现在它存在一个解决这个特定需求的库。是Konad;看看maybe section

            我将在这里报告文档中的示例用法:

            val foo: Int? = 1
            val bar: String? = "2"
            val baz: Float? = 3.0f
            
            fun useThem(x: Int, y: String, z: Float): Int = x + y.toInt() + z.toInt()
            
            val result: Int? = ::useThem.curry() 
               .on(foo.maybe) 
               .on(bar.maybe) 
               .on(baz.maybe)
               .nullable
            
            // or even 
            
            val result: Result<Int> = ::useThem.curry() 
               .on(foo.ifNull("Foo should not be null")) 
               .on(bar.ifNull("Bar should not be null")) 
               .on(baz.ifNull("Baz should not be null"))
               .result
            

            【讨论】:

              【解决方案14】:

              对于要检查的任何数量的值,您都可以使用:

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

              发送到块的元素使用通配符,如果要访问值,则需要检查类型,如果只需要使用一种类型,则可以将其更改为泛型

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2019-09-03
                • 2023-03-15
                • 1970-01-01
                • 2013-01-16
                • 1970-01-01
                • 2018-08-11
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多