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

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

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

总结在前面

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

函数 对象引用 返回值 是否是扩展函数
let it Lambda表达式结果
run this Lambda表达式结果
run - Lambda表达式结果 不是:调用无需上下文对象
with this Lambda表达式结果 不是:把上下文对象当做参数
apply this 上下文对象
also it 上下文对象

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

  • 对一个非空(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 调用同一个对象的多个方法 属性时,可以省去对象名,直接调用方法、访问属性 需要多次调用同一个对象的属性 方法 返回值=最后一行 return表达式
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")
    }
}

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


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354

推荐阅读更多精彩内容