初学Kotlin可能对let,run,apply等方法有一些疑惑,为什么每个对象都能引用这些方法?什么时候可以使用这些方法?本篇文章可以作为使用手册参考,新手建议收藏。
本篇文章我们我们可以掌握下面这些知识点:
- 什么是内联函数?有何特点?
- 函数类型有哪几种?什么是带接收者的函数类型?
- 掌握run、let、with、apply、also的使用场景
内联函数
let、run、apply等方法位于kotlin的标准库中,并且被定义为内联函数。kotlin 的内联函数是一种特殊的函数,在编译时会被展开到调用它的位置,而不是像普通函数那样通过函数调用的方式执行。内联函数的主要目的是减少函数调用的开销,特别涉及到高阶函数(即函数作为参数或返回值的函数)时。
关联C++: 内联函数可以理解为C++ 中的宏定义,在编译时是将所有的宏引用的地方全部替换为宏对应的表达式。
总的来说,虽然我们在代码中引用了内联函数,但是在编译文件(kotlin对应的class文件)中是找不到内联函数的,有的只是内联函数对应的真实表达式。
举个例子
我们对一个String对象执行run方法,在run方法中打印一个字符串。如下所示
@Test
fun testInline() {
"hello, inline".run {
print("$this, this is run inline method \n")
}
}
执行用例
输出如下结果:hello, inline, this is run inline method
查看编译class文件
AndroidStudio 中可以通过 "Tools->Kotlin->Show Kotlin Bytecode" 来查看class文件内容,如下图所示:
我们可以点击Kotlin Bytecode界面的Decompile 来查看反编译kotlin源文件的内容,更好理解,如下所示
我们可以看到testInline测试方法中仅仅只是定义了1个变量并通过print打印而已,完全看不到内联函数run的踪影。这也证实了内联函数在编译时会替换为其表达式具体的内容(该例子中就替换为了字符串的打印)。
扩展函数
kotlin 中引入了扩展函数的概念,即在不修改类任何代码的情况下支持扩展额外功能。
扩展函数的格式为:fun 类名.扩展方法名(方法参数):返回值 {}
比如,定义一个空的 MyClass 类,如下所示
class MyClass {
}
我们针对MyClass 写一个扩展函数 MyClass.helloExtend(),同时写一个测试用例,如下所示:
@Test
fun testExtend() {
MyClass().helloExtend()
}
fun MyClass.helloExtend():Unit {
print("hello extend method\n")
}
可以看到,MyClass 并没有定义helloExtend 方法,但是我们给其定义了这个扩展方法之后,我们就可以直接引用了,注意:我们在扩展方法中能够通过 this关键字引用到MyClass 对象的实例。
执行用例结果如下
若是要对所有class 都扩展一个helloExtend 方法,要怎么办呢?我们可以通过泛型来实现扩展,如下所示:
@Test
fun testExtend() {
MyClass().helloExtend()
"i am a String".helloExtend()
}
fun <T> T.helloExtend():Unit {
print("hello extend method. $this \n")
}
我们将扩展的class用泛型T来表示,就可以将helloExtend 方法扩展到任意class上了。可以看到我们将 helloExtend() 方法应用到了字符串对象和MyClass对象上。执行用例结果如下:
函数类型
kotlin中函数类型可以分为普通函数类型 和 带接收者的函数类型
T.() -> R(带有接收者的函数类型)
-
定义:这是一个需要类型为
T的接收者对象的函数类型。函数体内部可以通过this直接访问接收者的成员,或隐式省略this。 -
举个例子
val stringToInt: String.() -> Int = { this.length } val str = "Hello" println(str.stringToInt()) // 输出 5(访问 String 的 length 属性)
() -> R(普通函数类型)
-
定义:这是一个不需要接收者的普通函数类型。函数体无法直接访问任何对象的成员(除非通过闭包捕获外部变量)。
-
举个例子:
val getFive: () -> Int = { 5 } println(getFive()) // 输出 5
内置标准函数
在kotlin 包下有一个 StandardKt.kt 文件。我们熟悉的run ,apply,let等方法就定义在该文件中。下面我们重点介绍该文件中的常用方法。
API声明
public inline fun <R> run(block: () -> R): Rpublic inline fun <T, R> T.run(block: T.() -> R): Rpublic inline fun <T, R> with(receiver: T, block: T.() -> R): Rpublic inline fun <T> T.apply(block: T.() -> Unit): Tpublic inline fun <T> T.also(block: (T) -> Unit): Tpublic inline fun <T, R> T.let(block: (T) -> R): R
我们以run方法为例来说明来看看扩展函数,带有接收者的函数类型是如何定义和使用的。
run
定义
/**
* Calls the specified function [block] and returns its result.
*
* For detailed usage information see the documentation for [scope functions](https://kotlinlang.org/docs/reference/scope-functions.html#run).
*/
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
/**
* Calls the specified function [block] with `this` value as its receiver and returns its result.
*
* For detailed usage information see the documentation for [scope functions](https://kotlinlang.org/docs/reference/scope-functions.html#run).
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
这2个run 方法有主要有下面几个区别
| 特性 | T.run(block: T.() -> R): R | run(block: () -> R): R |
|---|---|---|
| 是否是扩展函数 | 是 | 不是 |
| 是否需要接收者 | 需要 T 的实例作为接收者 | 不需要接收者 |
| 访问成员的方式 | 通过 this 或隐式访问接收者 | 需通过闭包捕获或参数传递 |
| 调用方式 | receiver.block() | block() |
测试
我们通过测试代码验证其区别
@Test
fun testRun() {
run {
print("1 this is in $this run block\n")
}
print("2 this is in $this run block\n")
MyClass().run {
helloExtend()
print("3 this is in $this run block\n")
}
}
执行用例,结果如下:
run:
由此我们可以得出如下结论:
- run{}: 独立的run语句块与在方法中执行效果是一模一样的,其中this 指针都是指向引用class类的。所以独立的run 方法可以起到代码内聚的作用,当一个方法中有多个不同操作时,可以通过run方法将其分开。
- MyClass().run{}: 当run方法作为类的扩展方法引用时,其内部的this 指针是指向接收者对象(此处为MyClass),可以在run方法中调用该引用类的方法,比如上面的 helloExtend()。
run、let、with、apply 和 also 使用场景
在 Kotlin 中,run、let、with、apply 和 also 是常用的作用域函数(Scope Functions) ,它们都用于在某个对象的上下文中执行代码,但行为和适用场景略有不同。以下是它们的核心区别和使用场景总结:
1. run
-
语法:
val result = object.run { // 代码块(this 指向 object) // 返回最后一行结果 } -
特点:
- 接收者:
this(隐式访问对象成员)。 - 返回值:代码块的最后一行结果。
- 接收者:
-
使用场景:
-
对象初始化并返回计算结果:
val length = "Hello".run { println("字符串长度:$length") // 直接访问 length length // 返回长度 } -
空安全调用(结合
?.run):val nullableString: String? = "Kotlin" nullableString?.run { println(length) } // 非空时执行
-
2. let
-
语法:
val result = object.let { it -> // 代码块(it 指向 object) // 返回最后一行结果 } -
特点:
- 接收者:
it(显式访问对象)。 - 返回值:代码块的最后一行结果。
- 接收者:
-
使用场景:
-
空安全处理(常用
?.let):val nullableString: String? = "Kotlin" nullableString?.let { println(it.length) // 非空时执行 } -
转换对象:
val number = "123".let { it.toInt() } // 返回 Int 类型
-
3. with
-
语法:
val result = with(object) { // 代码块(this 指向 object) // 返回最后一行结果 } -
特点:
- 接收者:
this(隐式访问对象成员)。 - 返回值:代码块的最后一行结果。
- 非扩展函数,需显式传入对象。
- 接收者:
-
使用场景:
-
批量操作对象属性:
val person = Person() val info = with(person) { name = "Alice" age = 30 "姓名:$name,年龄:$age" // 返回字符串 }
-
4. apply
-
语法:
val result = object.apply { // 代码块(this 指向 object) // 返回 object 本身 } -
特点:
- 接收者:
this(隐式访问对象成员)。 - 返回值:对象本身(
this)。
- 接收者:
-
使用场景:
-
对象初始化(类似 Builder 模式):
val person = Person().apply { name = "Bob" age = 25 } -
链式调用:
val button = Button(context).apply { text = "Click" setOnClickListener { /* ... */ } }
-
5. also
-
语法:
val result = object.also { it -> // 代码块(it 指向 object) // 返回 object 本身 } -
特点:
- 接收者:
it(显式访问对象)。 - 返回值:对象本身(
it)。
- 接收者:
-
使用场景:
-
副作用操作(如日志、验证):
val list = mutableListOf(1, 2, 3).also { println("初始化列表:$it") } -
链式中间操作:
val file = File("path").also { require(it.exists()) { "文件不存在" } }
-
总结对比表
| 函数 | 接收者 | 返回值 | 典型场景 |
|---|---|---|---|
run | this | 代码块最后一行 | 对象操作 + 返回结果 |
let | it | 代码块最后一行 | 空安全调用 + 参数传递 |
with | this | 代码块最后一行 | 非扩展函数,批量操作对象属性 |
apply | this | 对象本身 | 对象初始化(Builder 模式) |
also | it | 对象本身 | 副作用操作(日志、验证) |
如何选择?
-
是否需要返回值:
- 需要结果 →
run/let/with。 - 需要对象本身 →
apply/also。
- 需要结果 →
-
是否需要空安全:
- 空安全调用 →
?.let或?.run。
- 空安全调用 →
-
是否访问接收者成员:
- 隐式访问 →
run/apply/with。 - 显式访问 →
let/also。
- 隐式访问 →
示例代码
1. apply 初始化对象
val recyclerView = RecyclerView(context).apply {
layoutManager = LinearLayoutManager(context)
adapter = MyAdapter()
addItemDecoration(DividerItemDecoration(context, VERTICAL))
}
2. let 空安全转换
val input: String? = "42"
val number = input?.let { it.toIntOrNull() } ?: 0
3. also 记录日志
val data = fetchData().also {
log("获取数据:$it")
}
4. with 批量操作
val result = with(StringBuilder()) {
append("Hello")
append(" ")
append("Kotlin")
toString() // 返回拼接结果
}
掌握这些函数的使用场景,可以显著提升 Kotlin 代码的简洁性和可读性!