kotlin作用域函数:run、let、also、apply、with

刚开始学习 kotlin 的时候,对于这些作用域函数一头雾水,搞不明白为什么要弄出来这么多东西。现在来看看他们具体的区别以及适用的场景。 Kotlin 标准库包含几个函数,它们的唯一目的是在对象的上下文中执行代码块。 当对一个对象调用这样的函数并提供一个lambda表达式时,它会形成一个临时作用域。在此作用域中,可以访问该对象而无需其名称。这些函数称为作用域函数。 共有以下五种:letrunwithapply以及also。 废话不多说,先把从 kotlin 官方上扒拉下来的结论放这里

作用域函数中文版
作用域函数英文版

总结在前面

文章太长太啰嗦,直接看这里的结论:

函数对象引用返回值是否是扩展函数
letitLambda表达式结果
runthisLambda表达式结果
run-Lambda表达式结果不是:调用无需上下文对象
withthisLambda表达式结果不是:把上下文对象当做参数
applythis上下文对象
alsoit上下文对象

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

  • 对一个非空(non-null)对象执行 lambda 表达式:let
  • 将表达式作为变量引入为局部作用域中:let
  • 对象配置:apply
  • 对象配置并且计算结果:run
  • 在需要表达式的地方运行语句:非扩展的 run
  • 附加效果:also
  • 一个对象的一组函数调用:with 不同作用域函数的使用场景存在重叠,可以根据项目或团队中使用的特定约定来选择使用哪些函数。

虽然作用域函数可以让代码更加简洁,但是要避免过度使用它们:这会使代码难以阅读并可能导致错误。 我们还建议避免嵌套作用域函数,同时链式调用它们时要小心:因为很容易混淆当前上下文对象与thisit的值。

使用示例

假如我们有这么一个数据类

data class Book(var name: String, var price: Int) {
    fun changePrice(price: Int) {
        this.price = price
    }
}
val book = Book("book name", 68)

函数声明

public inline fun <T> T.also(block: (T) -> Unit): T
public inline fun <T> T.apply(block: T.() -> Unit): T 

public inline fun <T, R> T.let(block: (T) -> R): R
public inline fun <T, R> T.run(block: T.() -> R): R

public inline fun <R> run(block: () -> R): R

public inline fun <T, R> with(receiver: T, block: T.() -> R): R

我们把看起来相近的作用域函数的声明放在一块对比着看,看到这里就清楚了的就不要往下看了,看了也是浪费时间。

also

函数声明

public inline fun <T> T.also(block: (T) -> Unit): T

also函数是对泛型 T 的扩展函数,接收一个参数类型为T、无返回值(返回值为Unit类型)的函数,且also函数的返回值就是调用者。

  • 上下文对象作为 lambda 表达式的参数(it)来访问。
  • 返回值是上下文对象本身。

对于执行一些将上下文对象作为参数的操作很有用。 对于需要引用对象而不是其属性与函数的操作,或者不想屏蔽来自外部作用域的 this 引用时,请使用 also。 当你在代码中看到 also 时,可以将其理解为并且用该对象执行以下操作

val alsoResult = book.also {
    it.changePrice(20)
    it.name = "alsoResult"
}
println("alsoResult $alsoResult")

这里打印结果是alsoResult Book(name=alsoResult, price=20),看源码的话,可以简单的里面为调用了一下传入的函数,然后返回了调用者

public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}

apply

函数声明

public inline fun <T> T.apply(block: T.() -> Unit): T

可以看得出来apply是泛型 T 的扩展函数,接收一个带有 T 类型接收者的无参、无返回值的函数,并且apply函数返回值就是 T 类型,也就是调用者的类型。因为这里参数中的 T 是作为接收者类型,而不是参数,所以在传入的函数中需要用this而非it来指代调用者。 用法和also相差无几,只不过一个是接收者类型,一个是参数。

  • 上下文对象 作为接收者(this)来访问。
  • 返回值 是上下文对象本身。

对于不返回值且主要在接收者(this)对象的成员上运行的代码块使用它。apply最常见的使用场景是用于对象配置。这样的调用可以理解为将以下赋值操作应用于对象

val applyResult = book.apply {
    changePrice(200)
    name = "applyResult"
}
println("applyResult $applyResult")

这里打印的结果是applyResult Book(name=applyResult, price=200). 源码也和also几乎一样

public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}

let

函数类型声明如下:

public inline fun <T, R> T.let(block: (T) -> R): R

可以看到,let 是对泛型 T 的扩展函数,该扩展函数接收一个函数参数,并且函数参数的接收一个 T 类型的参数,且返回值是 R 类型,也是let这个扩展函数的返回值类型。

  • 上下文对象作为 lambda 表达式的参数(it)来访问。
  • 返回值是 lambda 表达式的结果。
val letResult = book.let {
    it.changePrice(100)
    it.name = "letResult"
}
println("letResult $letResult")

这里传入的是一个 Lambda 表达式,前面说过,对于单参数值的Lambda 表达式,参数会被隐式声明为it,当然我们也可以指定一个具名意义的变量,比如

val letResult = book.let { bookEntry: Book ->
    bookEntry.changePrice(100)
    bookEntry.name = "letResult"
}

这里打印的结果是letResult kotlin.Unit。因为对于 Lambda 表达式来讲,如果最后一条语句是非赋值语句,则返回该语句的值;如果是赋值语句,则返回 Unit。 我们可以这么写来返回我们需要的值:

val letResult = book.let {
    it//返回值就是传入的 book 对象
}
val letResult = book.let {
    1//返回值就是1
}
val letResult = book.let {
     return@let 1//之前的文章中说过的显示指定返回值,是 1
}

从另外一个角度看,letalsoapply也差不多,只不过多了一个返回值类型,返回值就是传入的 Lambda 表达式的返回值 源码也差不了多少

public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

  • let 可用于在调用链的结果上调用一个或多个函数。
  • let 经常用于执行包含非空值代码块。如需对非空对象执行操作, 可对其使用安全调用操作符?.并调用 let 在 lambda 表达式中执行操作。
run

run这个函数给了两种方式

public inline fun <T, R> T.run(block: T.() -> R): R
public inline fun <R> run(block: () -> R): R

先看第一种,看起来就是把let中函数参数中的 T 类型参数改成了接收者类型,也是返回 R 类型;这和applyalso的区别是一样的。

  • 上下文对象 作为接收者(this)来访问。
  • 返回值 是 lambda 表达式结果。

当 lambda 表达式同时初始化对象并计算返回值时,run 很有用。

val runResult = book.run {
    name = "runResult"
    changePrice(110)
    this //作为返回值
}
println("runResult $runResult")

源码是这样的

public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

第二种

val otherRunResult =  run {
    Book("run", 120) //作为返回值
}
println("otherRunResult $otherRunResult")

源码

public inline fun <R> run(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

这也没啥好说的,只不过是这里并没有输入参数,只是可以使你在需要表达式的地方就可以执行一个语句。

with

函数声明

public inline fun <T, R> with(receiver: T, block: T.() -> R): R

with并不是扩展函数,需要传入一个T 类型的receiver,可以在 block 中访问这个receiver的方法和属性,

  • 上下文对象作为接收者(this)使用。
  • 返回值是 lambda 表达式结果。

建议当不需要使用 lambda 表达式结果时,使用 with 来调用上下文对象上的函数。 在代码中,with 可以理解为对于这个对象,执行以下操作.

val withResult = with(book) {
    changePrice(300)
    name = "withResult"
    this //作为返回值
}
println("withResult $withResult")

这里的打印结果是withResult Book(name=withResult, price=300)

如何选择

这里再搬运一个总结的表格

函数名作用应用场景备注
let定义一个变量在特定作用域内
统一做判空处理明确一个变量所处特定的作用域范围内可使用
针对一个可空对象统一做判空处理区别在于返回值
let函数:返回值=最后一行return的表达式
also函数:返回值=传入对象本身
also
with调用同一个对象的多个方法属性时,可以省去对象名,直接调用方法、访问属性需要多次调用同一个对象的属性
run结合了let 函数和 with 函数的作用1.调用同一个对象的多个方法/属性时可以省去对象名重复,直接调用方法名 /属性即可
2.定义一个变量在特定作用域内
3.统一做判空处优点:避免了let函数必须使用it参数替代对象弥补了with函数无法判空的缺点
apply对象实例初始化时需要对对象中的属性进行赋值且返回该对象二者区别在于返回值:
run函数返回最后一行的值表达式
apply函数返回传入的对象的本身

另外一个角度的选择

it or this

每个作用域函数都使用以下两种方式之一来引用上下文对象

  1. 作为 lambda 表达式的接收者 (this)
  2. 作为 lambda 表达式的参数(it)

两者都提供了同样的功能,runwith以及apply通过关键字this将上下文对象引用为lambda表达式的接收者。 因此,在它们的lambda表达式中可以像在普通的类函数中一样访问上下文对象。在大多数场景,当你访问接收者对象时你可以省略this, 来让你的代码更简短。 相对地,如果省略了this,就很难区分接收者对象的成员及外部对象或函数。因此,对于主要对对象的成员进行操作(调用其函数或赋值其属性)的lambda表达式, 建议将上下文对象作为接收者(this)。 反过来,letalso将上下文对象引用为lambda表达式参数。如果没有指定参数名,对象可以用隐式默认名称it访问。itthis简短,带有it的表达式通常更易读。不过,当调用对象函数或属性时,不能像this这样隐式地访问对象。 因此,当上下文对象在作用域中主要用作函数调用中的参数时,通过it访问上下文对象会更好。 在代码块中使用多个变量时,it也更好一些。

返回值

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

apply 及 also 返回上下文对象。 let、run 及 with 返回 lambda 表达式结果. apply 及 also 的返回值是上下文对象本身。因此,它们可以作为辅助步骤包含在调用链中:可以继续在同一个对象上一个接一个地进行链式函数调用。

写在最后的注意事项

在最开始的红色部分也提高过尽量不要嵌套使用作用域函数,警惕引发的上下文混淆。看下面的代码猜一下打印结果是什么。

fun main() {
    val length = 0
    "hello".apply {
        println("this is apply $length")
        println("this is apply ${this.length}")
    }

    "hello".let {
        println("this is let $it")
        "world".also {
            println("this is run $it")
        }
    }

    fun innerFunc(){
        "hi".apply {
            println("this is innerFunc apply $length")
            println("this is innerFunc apply ${this.length}")

        }
    }
    innerFunc()
}

结果是如下:

this is apply 0 this is apply 5 this is let hello this is run world this is innerFunc apply 0 this is innerFunc apply 2

这里我们在写代码的时候,IDE 给了提示:Implicit parameter ‘it’ of enclosing lambda is shadowed

在这里插入图片描述
我们可以通过修改隐式 it 的名字来避免这个问题

"hello".let {
    println("this is let $it")
    "world".also { world->
        println("this is run $world")
    }
}

但最好还是避免这种嵌套调用的情况。


如何学习Kotlin

Kotlin作为一种现代的、静态类型的编程语言,拥有诸多独特且强大的特性,虽然Kotlin语法简洁,但是想要深入理解他的新特性,熟练的使用在工作上面还是得要花费很大的时间成本来学习,因此我给大家准备了Kotlin从入门到精通高级Kotlin强化实战两份资料来帮助大家系统的学习Kotlin,需要的朋友扫描下方二维码,免费领取!!!

Kotlin从入门到精通

准备开始

  • 基本语法
  • 习惯用语
  • 编码风格在这里插入图片描述

基础

  • 基本类型
  • 控制流
  • 返回与跳转在这里插入图片描述

类和对象

  • 类和继承
  • 属性和字段
  • 接口
  • 可见性修饰词
  • 扩展
  • 数据对象
  • 在这里插入图片描述

函数和lambda表达式

  • 函数
  • 高级函数和lambda表达式
  • 内联函数在这里插入图片描述

其他

  • 多重申明
  • Ranges
  • 类型检查和自动转换
  • This表达式
  • 等式
  • 运算符重载
  • 在这里插入图片描述

互用性

  • 动态类型

工具

  • Kotlin代码文档
  • 使用Maven
  • 使用Ant
  • 使用Griffon
  • 使用Gradle在这里插入图片描述

FAQ

  • 与Java对比
  • 与Scala对比在这里插入图片描述

高级Kotlin强化实战

第一章 Kotlin入门教程

  • 1.Kotlin概述
  • 2.Kotlin与Java比较
  • 3.巧用Android Studio
  • 4.认识Kotlin基本类型
  • 5.走进Kotlin的数组
  • 6.走进Kotlin的集合
  • 7.集合问题
  • 8.完整代码
  • 9.基础语法在这里插入图片描述

第二章 Kotlin实战避坑指南

  • 2.1 方法入参是常量,不可修改
  • 2.2 不要 Companion 、INSTANCE ?
  • 2.3 Java 重载,在 Kotlin 中怎么巧妙过渡一下?
  • 2.4 Kotlin 中的判空姿势
  • 2.5 Kotlin 复写 Java 父类中的方法
  • 2.6 Kotlin “狠”起来,连TODO 都不放过!
  • 在这里插入图片描述

第三章 项目实战《Kotlin Jetpack实战》

  • 3.1 从一个膜拜大神的 Demo 开始
  • 3.2 Kotlin 写 Gradle 脚本是一种什么体验?
  • 3.3 Kotlin 编程的三重境界
  • 3.4 Kotlin 高阶函数
  • 3.5 Kotlin泛型
  • 3.6 Kotlin 扩展
  • 3.7 Kotlin 委托
  • 3.8 协程“不为人知”的调试技巧
  • 3.9 图解协程:suspend在这里插入图片描述
完整学习文档,可以扫描下方二维码免费领取!!!
  • 10
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值