Kotlin入门:run、let、with、apply、also的使用场景

213 阅读5分钟

初学Kotlin可能对let,run,apply等方法有一些疑惑,为什么每个对象都能引用这些方法?什么时候可以使用这些方法?本篇文章可以作为使用手册参考,新手建议收藏。

本篇文章我们我们可以掌握下面这些知识点:

  1. 什么是内联函数?有何特点?
  2. 函数类型有哪几种?什么是带接收者的函数类型?
  3. 掌握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 run用例结果.png

查看编译class文件

AndroidStudio 中可以通过 "Tools->Kotlin->Show Kotlin Bytecode" 来查看class文件内容,如下图所示:

查看编译class文件.png

我们可以点击Kotlin Bytecode界面的Decompile 来查看反编译kotlin源文件的内容,更好理解,如下所示

反编译.png 我们可以看到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 对象的实例。

执行用例结果如下

用例结果.png

若是要对所有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对象上。执行用例结果如下:

泛型用例结果.png

函数类型

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): R
  • public inline fun <T, R> T.run(block: T.() -> R): R
  • public inline fun <T, R> with(receiver: T, block: T.() -> R): R
  • public inline fun <T> T.apply(block: T.() -> Unit): T
  • public inline fun <T> T.also(block: (T) -> Unit): T
  • public 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): Rrun(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")
    }
}

执行用例,结果如下: 截屏2025-02-12 下午9.14.32.png run:

由此我们可以得出如下结论:

  1. run{}: 独立的run语句块与在方法中执行效果是一模一样的,其中this 指针都是指向引用class类的。所以独立的run 方法可以起到代码内聚的作用,当一个方法中有多个不同操作时,可以通过run方法将其分开。
  2. MyClass().run{}: 当run方法作为类的扩展方法引用时,其内部的this 指针是指向接收者对象(此处为MyClass),可以在run方法中调用该引用类的方法,比如上面的 helloExtend()。

runletwithapply 和 also 使用场景

在 Kotlin 中,runletwithapply 和 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()) { "文件不存在" } 
      }
      

总结对比表

函数接收者返回值典型场景
runthis代码块最后一行对象操作 + 返回结果
letit代码块最后一行空安全调用 + 参数传递
withthis代码块最后一行非扩展函数,批量操作对象属性
applythis对象本身对象初始化(Builder 模式)
alsoit对象本身副作用操作(日志、验证)

如何选择?

  1. 是否需要返回值

    • 需要结果 → run / let / with
    • 需要对象本身 → apply / also
  2. 是否需要空安全

    • 空安全调用 → ?.let 或 ?.run
  3. 是否访问接收者成员

    • 隐式访问 → 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 代码的简洁性和可读性!