细说Kotlin工具函数及使用准则—函数let()run()apply()with()also()

Posted LQS_Android

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了细说Kotlin工具函数及使用准则—函数let()run()apply()with()also()相关的知识,希望对你有一定的参考价值。

作⽤域函数

Kotlin 标准库包含⼏个函数,它们的唯⼀⽬的是在对象的上下⽂中执⾏代码块。当对⼀个对象调⽤这样的函数并提供⼀个 lambda 表达式 时,它会形成⼀个临时作⽤域。在此作⽤域中,可以访问该对象⽽⽆需其名称。这些函数称为作⽤域函数 。共有以下五种: let run with apply 以及 also
这些函数基本上做了同样的事情:在⼀个对象上执⾏⼀个代码块。不同的是这个对象在块中如何使⽤,以及整个表达式的结果是什么。
下⾯是作⽤域函数的典型⽤法 :
 
Person("Alice", 20, "Amsterdam").let {
    println(it)
    it.moveTo("London")
    it.incrementAge()
    println(it)
}

如果不使⽤ let 来写这段代码,就必须引⼊⼀个新变量,并在每次使⽤它时重复其名称。

   val alice = Person("Alice", 20, "Amsterdam")
   println(alice)
   alice.moveTo("London")
   alice.incrementAge()
   println(alice)
作⽤域函数没有引⼊任何新的技术,但是它们可以使你的代码更加简洁易读。
由于作⽤域函数的相似性质,为你的案例选择正确的函数可能有点棘⼿。选择主要取决于你的意图和项⽬中使⽤的⼀致性。下⾯我们将详细描述各种作⽤域函数及其约定⽤法之间的区别。
区别
由于作⽤域函数本质上都⾮常相似,因此了解它们之间的区别很重要。每个作⽤域函数之间有两个主要区别:
引⽤上下⽂对象的⽅式
返回值
上下⽂对象: this 还是 it
在作⽤域函数的 lambda 表达式⾥,上下⽂对象可以不使⽤其实际名称⽽是使⽤⼀个更简短的引⽤来访问。每个作⽤域函数都使⽤以下两种⽅式之⼀来访问上下⽂对象:作为 lambda 表达式的 接收者 this )或者作为lambda表达式的参数( it )。两者都提供了同样的功能,因此我们将针对不同的场景描述两者的优缺点,并提供使⽤建议。
fun main() {
     val str = "Hello"
     // this
     str.run {
           println("The receiver string length: $length")
          //println("The receiver string length: ${this.length}") // 和上句效果相同
     }

     // it
     str.let {
         println("The receiver string's length is ${it.length}")
     } 
}
this
run 、with 以及 apply 通过关键字 this 引⽤上下⽂对象。 因此,在它们的 lambda 表达式中可以像在普通的类函数中⼀样访问上下⽂对象。在⼤多数场景,当你访问接收者对象时你可以省略 this ,来让你的代码更简短。相对地,如果省略了 this ,就很难区分接收者对象的成员及外部对象或函数。因此,对于主要对对象成员进⾏操作(调⽤其函数或赋值其属性)的 lambda 表达式,建议将上下⽂对象作为接收者( this )。
   val adam = Person("Adam").apply {
       age = 20 // 和 this.age = 20 或者 adam.age = 20 ⼀样
       city = "London"
   }
   println(adam)

反过来,let 及 also 将上下⽂对象作为 lambda 表达式参数。如果没有指定参数名,对象可以⽤隐式默认名称 it 访问。it this 简短,带有 it 的表达式通常更容易阅读。然⽽,当调⽤对象函数或属性时,不能像this 这样隐式地访问对象。因此,当上下⽂对象在作⽤域中主要⽤作函数调⽤中的参数时,使⽤ it 作为上下⽂对象会更好。若在代码块中使⽤多个变量,则 it 也更好。

fun getRandomInt(): Int {
   return Random.nextInt(100).also {
       writeToLog("getRandomInt() generated value $it")
   }
}
  
val i = getRandomInt()

此外,当将上下⽂对象作为参数传递时,可以为上下⽂对象指定在作⽤域内的⾃定义名称。

fun getRandomInt(): Int {
   return Random.nextInt(100).also { value ->
       writeToLog("getRandomInt() generated value $value")
   }
}

val i = getRandomInt()

返回值

根据返回结果,作⽤域函数可以分为以下两类:

apply also 返回上下⽂对象。
let run with 返回 lambda 表达式结果。
这两个选项使你可以根据在代码中的后续操作来选择适当的函数。
上下⽂对象
apply also 的返回值是上下⽂对象本⾝。因此,它们可以作为辅助步骤包含在调⽤链中:你可以继续在同⼀个对象上进⾏链式函数调⽤。
 val numberList = mutableListOf<Double>()

 numberList.also { println("Populating the list") }.apply {
    add(2.71)
    add(3.14)
    add(1.06)
 }.also { println("Sorting the list") }
  .sort()

它们还可以⽤在返回上下⽂对象的函数的 return 语句中。

fun getRandomInt(): Int {
    return Random.nextInt(100).also {
         writeToLog("getRandomInt() generated value $it")
    }
}

val i = getRandomInt()

Lambda 表达式结果

let run with 返回 lambda 表达式的结果。所以,在需要使⽤其结果给⼀个变量赋值,或者在需要对其结果进⾏链式操作等情况下,可以使⽤它们。
val numbers = mutableListOf("one", "two", "three")

val countEndsWithE = numbers.run {
    add("four")
    add("five")
    count { it.endsWith("e") }
}

println("There are $countEndsWithE elements that end with e.")

此外,还可以忽略返回值,仅使⽤作⽤域函数为变量创建⼀个临时作⽤域。

val numbers = mutableListOf("one", "two", "three")

with(numbers) {
   val firstItem = first()
   val lastItem = last()
   println("First item: $firstItem, last item: $lastItem")
}

⼏个函数

为了帮助你为你的场景选择合适的作⽤域函数,我们会详细地描述它们并且提供⼀些使⽤建议。从技术⻆度来说,作⽤域函数在很多场景⾥是可以互换的,所以这些⽰例展⽰了定义通⽤使⽤⻛格的约定⽤法。
let函数
 
上下⽂对象 作为 lambda 表达式的参数( it )来访问。 返回值 lambda 表达式的结果。
let 可⽤于在调⽤链的结果上调⽤⼀个或多个函数。例如,以下代码打印对集合的两个操作的结果:
val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)

使⽤ let ,可以写成这样:

val numbers = mutableListOf("one", "two", "three", "four", "five")
 // 如果需要可以调⽤更多函数
numbers.map { it.length }.filter { it > 3 }.let { println(it) }

若代码块仅包含以 it 作为参数的单个函数,则可以使⽤⽅法引⽤( :: )代替 lambda 表达式:

val numbers = mutableListOf("one", "two", "three", "four", "five")

numbers.map { it.length }.filter { it > 3 }.let(::println)

let 经常⽤于仅使⽤⾮空值执⾏代码块。如需对⾮空对象执⾏操作,可对其使⽤安全调⽤操作符 ?. 并调⽤let 在 lambda 表达式中执⾏操作。

val str: String? = "Hello"
//processNonNullString(str) // 编译错误:str 可能为空
val length = str?.let {
   println("let() called on $it")
   processNonNullString(it) // 编译通过:'it' 在 '?.let { }' 中必不为空
   it.length
}

使⽤ let 的另⼀种情况是引⼊作⽤域受限的局部变量以提⾼代码的可读性。如需为上下⽂对象定义⼀个新变量,可提供其名称作为 lambda 表达式参数来替默认的 it

val numbers = listOf("one", "two", "three", "four")

val modifiedFirstItem = numbers.first().let { firstItem ->
     println("The first item of the list is '$firstItem'")
     if (firstItem.length >= 5) firstItem else "!" + firstItem + "!"
}.toUpperCase()

println("First item after modifications: '$modifiedFirstItem'") 

with函数

⼀个⾮扩展函数:上下⽂对象作为参数传递,但是在 lambda 表达式内部,它可以作为接收者( this )使⽤。回值lambda 表达式结果。

我们建议使⽤ with 来调⽤上下⽂对象上的函数,⽽不使⽤ lambda 表达式结果。在代码中, with 可以理解为“ 对于这个对象,执⾏以下操作。
 
val numbers = mutableListOf("one", "two", "three")

with(numbers) {
   println("'with' is called with argument $this")
   println("It contains $size elements")
}

with 的另⼀个使⽤场景是引⼊⼀个辅助对象,其属性或函数将⽤于计算⼀个值。

val numbers = mutableListOf("one", "two", "three")

val firstAndLast = with(numbers) {
    "The first element is ${first()}," +
    " the last element is ${last()}"
}

println(firstAndLast)

run函数

上下⽂对象 作为接收者( this )来访问。 返回值 lambda 表达式结果。
run with 做同样的事情,但是调⽤⽅式和 let ⼀样 —— 作为上下⽂对象的扩展函数 .
lambda 表达式同时包含对象初始化和返回值的计算时, run 很有⽤。
val service = MultiportService("https://example.kotlinlang.org", 80)

val result = service.run {
    port = 8080
    query(prepareRequest() + " to port $port")
}

// 同样的代码如果⽤ let() 函数来写:
val letResult = service.let {
    it.port = 8080
    it.query(it.prepareRequest() + " to port ${it.port}")
}

除了在接收者对象上调⽤ run 之外,还可以将其⽤作⾮扩展函数。⾮扩展 run 可以使你在需要表达式的地⽅执⾏⼀个由多个语句组成的块。

val hexNumberRegex = run {
   val digits = "0-9"
   val hexDigits = "A-Fa-f"
   val sign = "+-"
   Regex("[$sign]?[$digits$hexDigits]+")
}

for (match in hexNumberRegex.findAll("+1234 -FFFF not-a-number")) {
    println(match.value)
}

apply函数

上下⽂对象 作为接收者( this )来访问。 返回值 是上下⽂对象本⾝。
对于不返回值且主要在接收者( this )对象的成员上运⾏的代码块使⽤ apply apply 的常⻅情况是对象配置。这样的调⽤可以理解为“ 将以下赋值操作应⽤于对象
 
val adam = Person("Adam").apply {
   age = 32
   city = "London"
}

println(adam)

将接收者作为返回值,你可以轻松地将 apply 包含到调⽤链中以进⾏更复杂的处理。

also
上下⽂对象 作为 lambda 表达式的参数( it )来访问。 返回值 是上下⽂对象本⾝。
also 对于执⾏⼀些将上下⽂对象作为参数的操作很有⽤。对于需要引⽤对象⽽不是其属性与函数的操作,或者不想屏蔽来⾃外部作⽤域的 this 引⽤时,请使⽤ also
当你在代码中看到 also 时,可以将其理解为 并且⽤该对象执⾏以下操作
val numbers = mutableListOf("one", "two", "three")

numbers.also { println("The list elements before adding new one: $it") }.add("four")
函数选择

总结了这么多,为了帮助你选择合适的作⽤域函数,我们提供了它们之间的主要区别表。

以下是根据预期⽬的选择作⽤域函数的简短指南:

①对⼀个⾮空( non-null )对象执⾏ lambda 表达式: let
②将表达式作为变量引⼊为局部作⽤域中: let
③对象配置: apply
④对象配置并且计算结果: run
⑤在需要表达式的地⽅运⾏语句:⾮扩展的 run
⑥附加效果: also
⑦⼀个对象的⼀组函数调⽤: with
不同函数的使⽤场景存在重叠,你可以根据项⽬或团队中使⽤的特定约定选择函数。
尽管作⽤域函数是使代码更简洁的⼀种⽅法,但请避免过度使⽤它们:这会降低代码的可读性并可能导致错误。
避免嵌套作⽤域函数,同时链式调⽤它们时要⼩⼼:此时很容易对当前上下⽂对象及 this it 的值感到困惑。
其他函数
takeIf takeUnless
除了作⽤域函数外,标准库还包含函数 takeIf takeUnless 。这俩函数使你可以将对象状态检查嵌⼊到调⽤链中。
 
当以提供的谓词在对象上进⾏调⽤时,若该对象与谓词匹配,则 takeIf 返回此对象。否则返回 null 。因此,takeIf 是单个对象的过滤函数。反之, takeUnless 如果不匹配谓词,则返回对象,如果匹配则返回null 。该对象作为 lambda 表达式参数( it )来访问。
val number = Random.nextInt(100)
val evenOrNull = number.takeIf { it % 2 == 0 }
val oddOrNull = number.takeUnless { it % 2 == 0 }
println("even: $evenOrNull, odd: $oddOrNull")

当在 takeIf takeUnless 之后链式调⽤其他函数,不要忘记执⾏空检查或安全调⽤( ?. ),因为他们的 返回值是可为空的。

val str = "Hello"
val caps = str.takeIf { it.isNotEmpty() }?.toUpperCase()
//val caps = str.takeIf { it.isNotEmpty() }.toUpperCase() // 编译错误
println(caps)

takeIf 及 takeUnless 与作⽤域函数⼀起特别有⽤。⼀个很好的例⼦是⽤ let 链接它们,以便在与给定谓词匹配的对象上运⾏代码块。为此,请在对象上调⽤ takeIf ,然后通过安全调⽤( ?. )调⽤ let 。对于与谓词不匹配的对象,takeIf 返回 null,并且不调⽤ let

fun displaySubstringPosition(input: String, sub: String) {
   input.indexOf(sub).takeIf { it >= 0 }?.let {
      println("The substring $sub is found in $input.")
      println("Its start position is $it.")
   }
}

displaySubstringPosition("010000011", "11")

displaySubstringPosition("010000011", "12")

没有标准库函数时,相同的函数看起来是这样的:

fun displaySubstringPosition(input: String, sub: String) {
      val index = input.indexOf(sub)

      if (index >= 0) {
         println("The substring $sub is found in $input.")
         println("Its start position is $index.")
      }
}

displaySubstringPosition("010000011", "11")

displaySubstringPosition("010000011", "12")

函数式编程常用API及解释:

函数式编程常见的转换函数map()、过滤函数filter()、聚合函数reduce()请参见下一篇:

 细说Kotlin工具函数及使用准则-转换函数map()、过滤函数filter()、聚合函数(二)

用知识铭记编程的岁月!遇见即是缘分,愿你我的脚步轻盈地踏过编程的花海!

以上是关于细说Kotlin工具函数及使用准则—函数let()run()apply()with()also()的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin差异化分析,let,run,with,apply及also

Kotlin差异化分析,let,run,with,apply及also

Kotlin篇差异化分析,let,run,with,apply及also

Kotlin之let,apply,with,run函数区别(转)

Kotlin let与run 使用场景

Kotlin基础(十三) 高级语法let函数apply函数also函数run函数和with函数