Kotlin run 详解:把对象操作收进作用域,再把结果带出来

简介

run 是 Kotlin 标准库里的作用域函数。

作用域函数常见有 5 个:

  • let
  • run
  • with
  • apply
  • also

run 的特点比较鲜明:

在对象作用域里执行一段逻辑,然后返回 Lambda 最后一行的结果。

常见写法:

kotlin 复制代码
val result = user.run {
    "$name-$age"
}

这里的 nameage 来自 user 对象,result 是 Lambda 最后一行的字符串。

run 适合这类场景:

  • 在一个对象里连续读取多个属性
  • 对对象做一段计算并返回结果
  • 可空对象非空时执行并返回结果
  • 创建一个临时作用域,收拢中间变量
  • 配合 apply 完成"先配置,再计算"

run 有两种形式

Kotlin 里有两种 run

对象扩展函数 run

kotlin 复制代码
val result = obj.run {
    // this 是 obj
    // 最后一行作为返回值
}

源码大致可以理解成:

kotlin 复制代码
public inline fun <T, R> T.run(block: T.() -> R): R {
    return block()
}

特点:

text 复制代码
Lambda 里用 this 访问对象
run 返回 Lambda 最后一行结果

顶层函数 run

kotlin 复制代码
val result = run {
    // 没有接收者对象
    // 最后一行作为返回值
}

源码大致可以理解成:

kotlin 复制代码
public inline fun <R> run(block: () -> R): R {
    return block()
}

特点:

text 复制代码
没有上下文对象
主要用于创建临时作用域
返回 Lambda 最后一行结果

基础用法

先定义一个简单的数据类:

kotlin 复制代码
data class User(
    val name: String,
    val age: Int
)

使用 run

kotlin 复制代码
fun main() {
    val user = User("Tom", 18)

    val info = user.run {
        "$name-$age"
    }

    println(info)
}

输出:

text 复制代码
Tom-18

run 代码块里,this 就是 user

下面两种写法等价:

kotlin 复制代码
val info = user.run {
    "$name-$age"
}
kotlin 复制代码
val info = user.run {
    "${this.name}-${this.age}"
}

this 通常可以省略。

run 的返回值

run 返回 Lambda 最后一行结果。

kotlin 复制代码
fun main() {
    val user = User("Tom", 18)

    val nextAge = user.run {
        age + 1
    }

    println(nextAge)
}

输出:

text 复制代码
19

再看一个多行示例:

kotlin 复制代码
fun main() {
    val user = User("Tom", 18)

    val result = user.run {
        val adult = age >= 18
        "name=$name, adult=$adult"
    }

    println(result)
}

输出:

text 复制代码
name=Tom, adult=true

最后一行:

kotlin 复制代码
"name=$name, adult=$adult"

就是整个 run 的返回值。

this 接收者

run 的扩展函数版本使用带接收者的 Lambda:

kotlin 复制代码
T.() -> R

所以 Lambda 里可以直接访问对象成员。

kotlin 复制代码
data class Product(
    val name: String,
    val price: Double,
    val discountRate: Double
)

fun main() {
    val product = Product("Keyboard", 300.0, 0.8)

    val finalPrice = product.run {
        price * discountRate
    }

    println(finalPrice)
}

输出:

text 复制代码
240.0

如果使用 let,通常要通过 it 访问对象:

kotlin 复制代码
val finalPrice = product.let {
    it.price * it.discountRate
}

如果需要频繁访问同一个对象的属性,runthis 风格会更紧凑。

和 let 的区别

runlet 都返回 Lambda 结果。

区别在于上下文对象:

函数 上下文对象 返回值 常见用途
run this Lambda 结果 在对象作用域里计算结果
let it Lambda 结果 空安全、链式转换、显式参数

示例:

kotlin 复制代码
val user = User("Tom", 18)

val a = user.run {
    "$name-$age"
}

val b = user.let {
    "${it.name}-${it.age}"
}

println(a)
println(b)

输出:

text 复制代码
Tom-18
Tom-18

对象属性较多时,run 可以少写对象名或 it

参数名能提升可读性时,let 更合适:

kotlin 复制代码
val result = user.let { currentUser ->
    "${currentUser.name}-${currentUser.age}"
}

和 apply 的区别

runapply 都使用 this

区别在于返回值。

函数 上下文对象 返回值 常见用途
run this Lambda 结果 计算结果
apply this 对象本身 初始化、配置对象

示例:

kotlin 复制代码
data class ServerConfig(
    var host: String = "",
    var port: Int = 0
)

fun main() {
    val config = ServerConfig().apply {
        host = "127.0.0.1"
        port = 8080
    }

    val address = config.run {
        "$host:$port"
    }

    println(address)
}

输出:

text 复制代码
127.0.0.1:8080

apply 负责把对象配置好,run 负责从对象中计算一个结果。

可空对象配合 run

run 可以和安全调用符 ?. 配合使用。

kotlin 复制代码
data class Address(
    val city: String,
    val street: String
)

data class Customer(
    val name: String,
    val address: Address?
)

fun loadCustomer(id: Int): Customer? {
    return if (id == 1) {
        Customer("Tom", Address("上海", "人民路"))
    } else {
        null
    }
}

fun main() {
    val addressText = loadCustomer(1)?.run {
        val cityName = address?.city ?: "未知城市"
        "$name - $cityName"
    } ?: "客户不存在"

    println(addressText)
}

输出:

text 复制代码
Tom - 上海

含义是:

text 复制代码
客户非空 -> 进入 run 代码块并返回字符串
客户为空 -> 返回 客户不存在

顶层 run:临时作用域

顶层 run 没有接收者对象,主要用于创建一个临时作用域。

kotlin 复制代码
fun main() {
    val result = run {
        val a = 10
        val b = 20
        a + b
    }

    println(result)
}

输出:

text 复制代码
30

ab 只在 run 代码块里有效,不会泄漏到外层作用域。

顶层 run:表达式化分支逻辑

多步计算可以用顶层 run 收成一个表达式。

kotlin 复制代码
fun formatInput(input: String?): String {
    return run {
        val value = input?.trim().orEmpty()

        if (value.isBlank()) {
            "EMPTY"
        } else {
            value.uppercase()
        }
    }
}

fun main() {
    println(formatInput("  kotlin  "))
    println(formatInput("   "))
}

输出:

text 复制代码
KOTLIN
EMPTY

这里的 run 只是为了收拢临时变量 value,让函数保持表达式风格。

链式调用中使用 run

run 可以放在链式调用末尾,把前面的结果转换成最终结果。

kotlin 复制代码
fun main() {
    val total = listOf(1, 2, 3, 4, 5, 6)
        .filter { it % 2 == 0 }
        .map { it * it }
        .run {
            println("平方后的偶数: $this")
            sum()
        }

    println(total)
}

输出:

text 复制代码
平方后的偶数: [4, 16, 36]
56

这里的 this 是前面链式调用得到的集合。

run 的最后一行 sum() 返回 Int,所以 totalInt

实战 Demo:订单金额计算

kotlin 复制代码
data class OrderItem(
    val sku: String,
    val quantity: Int,
    val price: Double
)

data class Order(
    val id: String,
    val items: List<OrderItem>,
    val couponAmount: Double = 0.0
)

fun calculatePayAmount(order: Order): Double {
    return order.run {
        val total = items.sumOf { it.quantity * it.price }
        val afterCoupon = total - couponAmount

        if (afterCoupon < 0) {
            0.0
        } else {
            afterCoupon
        }
    }
}

fun main() {
    val order = Order(
        id = "A001",
        items = listOf(
            OrderItem("BOOK", 2, 59.0),
            OrderItem("PEN", 3, 5.0)
        ),
        couponAmount = 20.0
    )

    println(calculatePayAmount(order))
}

输出:

text 复制代码
113.0

run 把订单相关计算放在 Order 对象作用域里,最后返回应付金额。

实战 Demo:生成用户展示信息

kotlin 复制代码
data class UserProfile(
    val id: Long,
    val nickname: String?,
    val email: String?,
    val vipLevel: Int
)

fun buildDisplayName(profile: UserProfile): String {
    return profile.run {
        val name = nickname?.takeIf { it.isNotBlank() }
            ?: email?.substringBefore("@")
            ?: "用户$id"

        val vipText = if (vipLevel > 0) "VIP$vipLevel" else "普通用户"

        "$name ($vipText)"
    }
}

fun main() {
    val profile = UserProfile(
        id = 1001,
        nickname = null,
        email = "tom@example.com",
        vipLevel = 2
    )

    println(buildDisplayName(profile))
}

输出:

text 复制代码
tom (VIP2)

这里需要读取 profile 的多个字段,并返回一个新字符串,适合使用 run

实战 Demo:配置后返回连接地址

kotlin 复制代码
data class DbConfig(
    var host: String = "",
    var port: Int = 3306,
    var database: String = "",
    var username: String = ""
)

fun main() {
    val jdbcUrl = DbConfig()
        .apply {
            host = "127.0.0.1"
            port = 3306
            database = "demo"
            username = "root"
        }
        .run {
            "jdbc:mysql://$host:$port/$database?user=$username"
        }

    println(jdbcUrl)
}

输出:

text 复制代码
jdbc:mysql://127.0.0.1:3306/demo?user=root

apply 返回配置好的对象,run 返回基于对象生成的字符串。

实战 Demo:读取配置并转换

kotlin 复制代码
fun readConfig(key: String): String? {
    val configs = mapOf(
        "server.port" to "8080",
        "server.timeout" to " 30 ",
        "feature.debug" to "true"
    )

    return configs[key]
}

data class ServerOptions(
    val port: Int,
    val timeoutSeconds: Int,
    val debug: Boolean
)

fun loadOptions(): ServerOptions {
    return run {
        val port = readConfig("server.port")?.toIntOrNull() ?: 80
        val timeout = readConfig("server.timeout")?.trim()?.toIntOrNull() ?: 10
        val debug = readConfig("feature.debug")?.toBooleanStrictOrNull() ?: false

        ServerOptions(
            port = port,
            timeoutSeconds = timeout,
            debug = debug
        )
    }
}

fun main() {
    println(loadOptions())
}

输出:

text 复制代码
ServerOptions(port=8080, timeoutSeconds=30, debug=true)

顶层 runporttimeoutdebug 这些临时变量只存在于配置加载代码块里。

实战 Demo:报表统计

kotlin 复制代码
data class PayOrder(
    val id: String,
    val city: String,
    val amount: Double,
    val paid: Boolean
)

data class CityReport(
    val city: String,
    val orderCount: Int,
    val totalAmount: Double
)

val orders = listOf(
    PayOrder("A001", "上海", 120.0, true),
    PayOrder("A002", "北京", 80.0, false),
    PayOrder("A003", "上海", 260.0, true),
    PayOrder("A004", "深圳", 300.0, true),
    PayOrder("A005", "北京", 180.0, true)
)

需求:

  • 只统计已支付订单
  • 按城市分组
  • 生成城市报表
  • 返回报表摘要字符串

代码:

kotlin 复制代码
fun buildSummary(orders: List<PayOrder>): String {
    return orders
        .filter { it.paid }
        .groupBy { it.city }
        .map { (city, cityOrders) ->
            CityReport(
                city = city,
                orderCount = cityOrders.size,
                totalAmount = cityOrders.sumOf { it.amount }
            )
        }
        .sortedByDescending { it.totalAmount }
        .run {
            val totalOrders = sumOf { it.orderCount }
            val totalAmount = sumOf { it.totalAmount }
            val cityNames = joinToString { it.city }

            "城市=$cityNames, 订单数=$totalOrders, 总金额=$totalAmount"
        }
}

fun main() {
    println(buildSummary(orders))
}

输出:

text 复制代码
城市=上海, 深圳, 北京, 订单数=4, 总金额=860.0

run 接收前面链式处理后的报表列表,并把它转换成摘要字符串。

自定义 runIfNotNull

可空对象经常使用 ?.run { },也可以封装成扩展函数。

kotlin 复制代码
inline fun <T, R> T?.runIfNotNull(block: T.() -> R): R? {
    return this?.run(block)
}

data class Account(
    val id: Long,
    val email: String?
)

fun main() {
    val account: Account? = Account(1, "tom@example.com")

    val domain = account.runIfNotNull {
        email?.substringAfter("@")
    }

    println(domain)
}

输出:

text 复制代码
example.com

runIfNotNull 的返回值是 R?,对象为空时返回 null

run、let、apply、also 的区别

作用域函数主要看两个问题:

text 复制代码
Lambda 里怎么访问对象
函数返回什么
函数 上下文对象 返回值 常见用途
run this Lambda 结果 在对象作用域里计算结果
let it Lambda 结果 空安全、链式转换、显式参数
apply this 对象本身 初始化、配置对象
also it 对象本身 日志、调试、校验、附加动作
with this Lambda 结果 对已有对象集中调用

简单选择方式:

text 复制代码
需要在对象作用域里返回一个结果:run
需要用显式参数名做转换:let
需要初始化或配置对象:apply
需要附加动作并继续返回原对象:also

run 和 with 的关系

runwith 都使用 this,都返回 Lambda 结果。

区别是调用方式:

kotlin 复制代码
val result1 = user.run {
    "$name-$age"
}
kotlin 复制代码
val result2 = with(user) {
    "$name-$age"
}

run 是扩展函数,可以配合安全调用:

kotlin 复制代码
val result = userOrNull?.run {
    "$name-$age"
}

with 是普通函数,不适合直接写成 userOrNull?.with { } 这种形式。

常见注意点

run 返回最后一行,不返回对象本身

kotlin 复制代码
fun main() {
    val result = User("Tom", 18).run {
        age + 1
    }

    println(result)
}

输出:

text 复制代码
19

如果需要返回对象本身,使用 apply

kotlin 复制代码
val user = User("Tom", 18).apply {
    println(name)
}

只做日志时 also 更贴近语义

kotlin 复制代码
val result = listOf(1, 2, 3)
    .also { println(it) }
    .sum()

如果只是插入日志,并且要继续返回原对象,also 更合适。

顶层 run 不宜包住整段函数

kotlin 复制代码
fun createUser(): User {
    return run {
        val name = "Tom"
        val age = 18
        User(name, age)
    }
}

这段代码能运行。

如果整个函数只有这一段逻辑,直接写通常更简单:

kotlin 复制代码
fun createUser(): User {
    val name = "Tom"
    val age = 18
    return User(name, age)
}

顶层 run 更适合收拢局部临时变量,或者让某段逻辑作为表达式参与赋值。

嵌套 run 需要留意 this

kotlin 复制代码
outer.run {
    inner.run {
        // this 是 inner
    }
}

嵌套层级增加后,可以使用标签或者提取函数减少歧义。

kotlin 复制代码
outer.run outerBlock@{
    inner.run {
        println(this@outerBlock)
        println(this)
    }
}

总结

run 的核心可以记成几句话:

  • run 有两种形式:obj.run { }run { }
  • obj.run { } 里通过 this 访问对象
  • run 返回 Lambda 最后一行结果
  • ?.run { } 适合可空对象非空时计算结果
  • 顶层 run { } 适合创建临时作用域
  • 需要返回对象本身时,使用 apply
  • 只是插入日志或调试时,使用 also

run 的定位是:把一段对象相关逻辑收进作用域里,然后把计算结果作为表达式带出来。

相关推荐
杉氧15 小时前
Kotlin 协程深度解析②:生存指南——掌握结构化并发的生命线
android·kotlin
QING61821 小时前
Kotlin 协程新手指南 —— 协程上下文与调度器
android·kotlin·android jetpack
plainGeekDev1 天前
HttpURLConnection → OkHttp + Kotlin
android·java·kotlin
QING6181 天前
Kotlin 协程新手指南 —— 协程基础与挂起函数
android·kotlin·android jetpack
plainGeekDev1 天前
批量写入 → Room 事务
android·java·kotlin
杉氧1 天前
Kotlin 协程深度解析①:内核解密——揭秘 suspend 挂起函数的灵魂
android·kotlin
朝星1 天前
Android开发[11]:启动优化
android·kotlin
JohnnyDeng941 天前
【Android】Android渲染机制:Choreographer与VSYNC深度解析
android·性能优化·kotlin·jetpack
aidou13141 天前
Kotlin中实现星级评价选择功能(仅支持整数)
前端·kotlin·自定义view·imageview·ontouchevent·customratingbar