详解Kotlin中的作用域函数

Kotlin作用域函数

作用域函数,Kotlin在语法层面支持拓展函数,作用域函数,作用域函数是指对数据做一些变换的函数,与集合的操作符很相似,但集合的操作符只能作用域集合对象,而作用域函数可以操作任何对象。

Kotlin在语法层面为我们提供了: let, run, with, apply, and also几个作用域函数

展示一个作用域函数let:

data class Person(var name: String, var age: Int, var city: String) {
    fun moveTo(newCity: String) { city = newCity }
    fun incrementAge() { age++ }
}

fun main() {
    Person("Alice", 20, "Amsterdam").let {
        println(it)
        it.moveTo("London")
        it.incrementAge()
        println(it)
    }
}

如上所示我们向let方法中传入一个lambda表达式,值得注意的是我们在这里使用了一个it关键字,我们先暂时放下这个it关键字来试试假如不试用let方法想要获取一样的结果的方式

data class Person(var name: String, var age: Int, var city: String) {
    fun moveTo(newCity: String) { city = newCity }
    fun incrementAge() { age++ }
}

fun main() {
    val alice = Person("Alice", 20, "Amsterdam")
    println(alice)
    alice.moveTo("London")
    alice.incrementAge()
    println(alice)
}

可以看到在后者中我们每当对对象进行一次操作时,都需要传入对象的声明(对象名称),而在前面的代码中我们使用it替代了调用者Person()对象本身。当然也可以将对象声明为x或者任何你想要的名字

 Person("Alice", 20, "Amsterdam").let { x->
        println(x)
        x.moveTo("London")
        x.incrementAge()
        println(x)
    }
这样看起来就跟java中的lambda表达式十分相似了。

那么问题来了,如何区分或者该在什么时候调用这些作用域函数呢,毕竟他们看起来都差不多

Kotlin官方文档为我们提供了两种区分方式:

  • 引用上下文的方式(it还是this)
  • 函数的返回类型

先来看看it和this的区别:

fun main() {
    val str = "Hello"
    // this
    str.run {
        println("The receiver string length: $length")
        //println("The receiver string length: ${this.length}") // does the same
        //在这段代码中this指的是调用者本身(即lambda函数接受者)
    }

    // it
    str.let {
        println("The receiver string's length is ${it.length}")
        //这里的it值的是将调用者作为lambda函数所需参数传入,也可以写成
        println(x->"The receiver string's length is ${x.length}")
        
    }
}

通过上面的代码可以得知

  • 不同:
    •  let有闭包参数,run没有闭包参数
      
    •  let的闭包参数就是调用者本身,参数名为it
      
    •  run没有闭包参数,可以使用this指代调用者
      
    •  注:在let中,不可以使用this指代调用者
      

这里列出分别使用This作为调用者以及it作为闭包参数的方法

  • this
    • run
    • with<T>
    • apply
    • 值得注意的是,大多数时候this可以被省略,但如果这样的做法影响了代码可读性的话,建议使用this.
  • it
    • let
    • also

通过返回类型

  • 返回 上下文本身(指调用者)

    • apply
    • also
  • 返回lambda函数的执行结果

    • run
    • let
    • with
  • demo1:返回上下文对象

fun main() {
    val numberList = mutableListOf<Double>()
    numberList.also { println("Populating the list") }
            .apply {
                this.add(2.71)
                add(3.14)  //省略this
                add(1.0)
            }
            .also { println("Sorting the list") }
            .sort()
    println(numberList)
}
//查看一下also与apply的源代码
public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}
public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}

显而易见,使用apply或者also都可以返回调用者本身,使用这样的作用域函数可以非常方便的进行链式编码

  • dem02:返回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.")
//看代码大家都可以知道输出结果为3
//即countEndsWithE的数据类型为Int,这是因为返回了lambda表达式中count的返回类型
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

当然也可以选择无返回值(并不是没有执行结果)

fun main() {
    val numbers = mutableListOf("one", "two", "three")
    with(numbers) {
        val firstItem = first()
        val lastItem = last()        
        println("First item: $firstItem, last item: $lastItem")
    }
}

作用域函数的选择

参考kotolin官方文档:

函数 引用方式 Return value 是否为拓展函数
let it Lambda result Yes
run this Lambda result Yes
run - Lambda result No: called without the context object
with this Lambda result No: takes the context object as an argument.
apply this Context object Yes
also it Context object Yes

官方文档也建议你选择:

  • Executing a lambda on non-null objects(当非空对象需要执行闭包时): let
  • Introducing an expression as a variable in local scope(将表达式作为局部范围中的变量引入): let
  • Object configuration(对对象进行参数配置时): apply
  • Object configuration and computing the result(对对象进行配置并且需要得到执行结果): run
  • Running statements where an expression is required: non-extension(代码运行时需要非拓展性的表达式) run
  • Additional effects(添加附加效果): also
  • Grouping function calls on an object(对函数进行分组调用时): with

当然笔者也更建议读者采用阅读源码+代码实践的方式来验证

注意:虽然作用域函数可以使我们的代码变得更简洁,并且使得链式编程变得更加简单,但是建议不要在代码中过度使用作用域函数,这可能会导致代码的可读性降低,注意尽量不要使用嵌套函数,并且在使用链式编程的时候要小心混淆thisit,它们是十分容易被混淆的,而这可能会带来十分多的问题。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容