kotlin之let、with、run、apply、also

总结

let、with、run、apply、also 这几个的作用就是在一个对象的上下文中执行一段代码。当我们使用 lambda 表达式在一个对象上调用这样的一个函数时,它就形成了一个暂时的域,在这个域中,可以在不使用变量名的情况下获取到对象,因此,这些函数也被称为作用域函数(Scope functions)

run、with 和 apply 的上下文对象是 this ,可以省略掉 this 单词,因此主要用于操作对象成员(例如调用对象的方法或使用其属性)的时候

let 和 also 的上下文对象是 it , 适用于将此对象作为方法调用参数传入

apply 和 also 返回上下文对象,可用于链式调用返回上下文对象的函数的返回语句

let、run 和 with 返回 lambda 结果,可以在将结果分配给变量时使用

方法 上下文对象 返回值 扩展函数 使用场景
let it lambda 结果 在非空对象上执行一个 lambda或者在局部域中引入一个变量
with this lambda 结果 在一个对象上组合函数调用
run this lambda 结果 对象配置并计算结果
apply this 上下文对象 对象配置
also it 上下文对象 额外的效果
非扩展函数 run lambda 结果 在需要表达式的地方运行语句

表格里的上下文对象中,this 是可以省略的, it 是隐含的默认参数名,可以显式地指定为其他名字

let

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

可以看到第一句代码是 Contract ,这就需要引出 Contract 的内容

Contract 示例

fun String?.isNull(): Boolean {
    return this == null
}

fun test() {
    var str: String? = null

    if (str.isNull()) {
        str = "kotlin contract"
    }

    println(str.length)
}

如上的代码中,先为 String? 定义一个扩展函数 isNull(),用于判断是否为 null,在 test 函数中,声明一个 String? 类型的 str ,使用 isNull() 判断,如果其为 null ,就为其赋值,这样,在下面调用 str.length 的时候 str 就一定不会为 null ,但实际上,这里还是会编译报错:

Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?

这是因为编译器无法分析每个函数,不能得到 str 不为 null 的结果,也就无法将 String? 智能转换为 String ,Contract 就可以解决问题,它可以向编译器通知函数行为,上面的代码修改为如下,就不会报错了

@ExperimentalContracts
fun String?.isNull(): Boolean {
    //下面是添加的内容
    contract {
        returns(false) implies (this@isNull != null)
    }
    //上面是添加的内容
    return this == null
}

@ExperimentalContracts
fun test() {
    var str: String? = null

    if (str.isNull()) {
        str = "kotlin contract"
    }

    println(str.length)
}

上面代码中添加内容是告诉编译器:如果返回值为 false ,那么 this(函数的接收者)不为 null

Contract 的概念

Contract 是一种向编译器通知函数行为的方法,有以下特点:

  1. 只能在 top-level 函数体内使用 Contract
  2. Contract 所调用的声明必须是函数体内第一条语句
  3. Kotlin 编译器并不会验证 Contract,因此必须编写正确合理的 Contract
  4. 内联化的函数(也需要是 top-level 层级的函数)支持使用 Contract

Contract 的分类

Returns Contracts

表示当 return 的返回值是某个值(例如true、false、null)时,implies 后面的条件成立,有以下几种形式:

形式 说明
returns(value: Any?) implies 条件 如果函数返回值为 value,条件成立
returns() implies 条件 如果函数能够正常返回,且没有抛出异常,条件成立
returnsNotNull implies 条件 如果函数返回非 null 值,条件成立

CallsInPlace Contracts

CallsInPlace Contracts 允许开发者对调用的 lambda 表达式进行频率上的约束,只能在 inline 函数中调用

前面的高阶函数 let 就是一个 CallsInPlace Contracts

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

contract() 中的 callsInPlace 会告诉编译器,lambda 表达式 block 在 let 函数内只会执行一次

callsInPlace() 中的 InvocationKind 是一个枚举类,包含如下的枚举值

枚举值 说明
AT_MOST_ONCE 函数参数调用次数 <= 1
EXACTLY_ONCE 函数参数调用次数 == 1
AT_LEAST_ONCE 函数参数调用次数 >= 1
UNKNOWN 函数参数调用次数 不限制

以上 contract 内容就结束了,下面看 let 函数的实现,首先要明白两点:

  1. 在扩展函数内部,你可以像成员函数那样使用 this 来引用接收者对象
  2. 当 lambda 表达式只有一个参数,可以用 it 关键字来引用唯一的实参

源码

上面的源代码里可以看到:

let 函数是类型 T 的扩展函数,返回类型为 R,只有一个参数,即 block,类型为 (T) -> R,指代 参数为 T ,返回值为 R 的函数,因此上下文对象为 it ,指代 block 的唯一参数 T 。返回值为 block(this) ,是调用 block 指代的函数,并返回 block 函数的返回值,也就是返回 lambda 的结果

示例

在非空对象上执行一个 lambda

val str: String? = "Hello"   
//processNonNullString(str)       // compilation error: str can be null
val length = str?.let { 
    println("let() called on $it")        
    processNonNullString(it)      // OK: 'it' is not null inside '?.let { }'
    it.length
}

为了提高可读性,在局部域中引入一个变量

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 + "!"
}.uppercase()
println("First item after modifications: '$modifiedFirstItem'")

with

源码

@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}

with 函数不是扩展函数,而是将类型 T 的一个对象 receiver 作为参数传入 with 函数,同时传入 block 参数,block 的类型为 T.() -> R ,即 类型 T 的一个无参且返回值为类型 R 的扩展函数,而 block 函数的调用者就是前面传入的 receiver 参数,因此上下文对象为 this ,指代扩展函数的接收者 receiver ,返回值为 receiver.block() ,是调用 block 指代的函数,并返回 block 函数的返回值,也就是返回 lambda 的结果

示例

推荐使用情况:调用对象的方法和属性,但不返回结果。意味着 “with this object, do the following

val numbers = mutableListOf("one", "two", "three")
with(numbers) {
    println("'with' is called with argument $this")
    println("It contains $size elements")
}

使用对象的属性或方法计算出一个结果

val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
    "The first element is ${first()}," +
    " the last element is ${last()}"
}
println(firstAndLast)

run

run 有两个,一个是扩展函数 run ,一个是非扩展函数 run

源码

扩展函数 run

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

run 函数是类型 T 的一个扩展函数,返回值类型为 R,只有一个参数,即 block,类型为 T.() -> R ,即 T 的一个无参且返回值类型为 R 的扩展函数,因此上下文对象为 this ,指代调用 block 扩展函数的接收者,也就是调用 run 函数的对象,返回值为 block() ,是调用 block 指代的函数,并返回 block 函数的返回值,也就是返回 lambda 的结果

非扩展函数 run

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

run 函数不是扩展函数,返回类型为 R , run 函数的唯一参数 block 的类型为 () -> R ,即 一个无参且返回类型为 R 的函数类型,因此 没有上下文对象,返回值为 block() ,是调用 block 指代的函数,并返回 block 函数的返回值,也就是返回 lambda 的结果

示例

扩展函数 run

同时包含对象初始化和返回值的计算

val service = MultiportService("https://example.kotlinlang.org", 80)

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

非扩展函数 run

需要返回值的情况下执行由多个语句组成的块

val hexNumberRegex = run {
    val digits = "0-9"
    val hexDigits = "A-Fa-f"
    val sign = "+-"

    Regex("[$sign]?[$digits$hexDigits]+")
}

for (match in hexNumberRegex.findAll("+123 -FFFF !%*& 88 XYZ")) {
    println(match.value)
}

apply

源码

@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

apply 函数是类型 T 的一个扩展函数,且其返回值类型为 T 类型,唯一的参数 block 的类型为 T.() -> Unit,即 T 的一个无参无返回值扩展函数,因此上下文对象为 this ,指代调用 block 扩展函数的接收者,也就是调用 apply 函数的对象 ,返回值为 this,也就是调用者,即返回上下文对象

示例

对象配置。意味着 “apply the following assignments to the object

val adam = Person("Adam").apply {
    age = 32
    city = "London"        
}
println(adam)

also

源码

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

also 函数是类型 T 的一个扩展函数,且其返回值类型为 T 类型,block 参数的类型为 (T) -> Unit,即 参数为 T 且无返回值的函数,因此上下文对象为 it ,指代 block 的唯一参数 T ,返回值为 this,也就是调用者,即返回上下文对象

示例

将上下文作为参数。意味着 "and also do the following with the object"

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

推荐阅读更多精彩内容