Kotlin with 详解:把已有对象放进作用域集中处理

简介

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

作用域函数常见有 5 个:

  • let
  • run
  • with
  • apply
  • also

with 的定位很清楚:

把一个已有对象传进去,在这个对象的作用域里执行一段逻辑,然后返回 Lambda 最后一行的结果。

常见写法:

kotlin 复制代码
val result = with(user) {
    "$name-$age"
}

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

with 适合这类场景:

  • 对一个已经存在的对象连续读取多个属性
  • 对一个已经存在的对象连续调用多个方法
  • 在对象作用域里计算并返回结果
  • 生成摘要、报表、格式化文本
  • 测试代码中集中操作一个对象

with 的源码结构

Kotlin 标准库里,with 大致可以理解成这样:

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

这段代码有几个关键点:

  • receiver: T:传入的目标对象
  • block: T.() -> R:带接收者的 Lambda
  • receiver.block():在目标对象作用域里执行 Lambda
  • R:Lambda 的返回值类型

所以 with 的重点是两个:

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

with 不是扩展函数

withrun 很像,但调用方式不同。

run 是扩展函数:

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

with 是普通函数:

kotlin 复制代码
val result = with(user) {
    "$name-$age"
}

区别可以概括成:

text 复制代码
run:对象.run { }
with:with(对象) { }

因为 with 不是扩展函数,所以不能写成:

kotlin 复制代码
// 错误写法
// user.with { }

也不能写成:

kotlin 复制代码
// 错误写法
// user?.with { }

可空对象更常用 ?.run { },后面会单独说明。

基础用法

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

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

普通写法:

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

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

    println(info)
}

使用 with

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

    val info = with(user) {
        "$name-$age-$city"
    }

    println(info)
}

输出:

text 复制代码
Tom-18-上海

with(user) { } 里面,this 就是 user

下面两种写法等价:

kotlin 复制代码
val info = with(user) {
    "$name-$age-$city"
}
kotlin 复制代码
val info = with(user) {
    "${this.name}-${this.age}-${this.city}"
}

this 通常可以省略。

with 的返回值

with 返回 Lambda 最后一行结果。

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

    val nextAge = with(user) {
        age + 1
    }

    println(nextAge)
}

输出:

text 复制代码
19

再看一个多行示例:

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

    val result = with(user) {
        val adult = age >= 18
        "name=$name, city=$city, adult=$adult"
    }

    println(result)
}

输出:

text 复制代码
name=Tom, city=上海, adult=true

最后一行:

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

就是整个 with 的返回值。

with 和 run 的关系

with 可以理解成"普通函数版本的 run"。

两者都使用 this,都返回 Lambda 结果。

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

输出结果一样。

主要区别:

函数 调用方式 是否扩展函数 可空对象常见写法
run obj.run { } obj?.run { }
with with(obj) { } 先判空,或改用 ?.run { }

当对象已经明确非空,并且需要集中访问它的成员时,with 表达很直接。

对象可能为空时,?.run { } 通常更顺手。

可空对象和 with

with 不是扩展函数,所以没有 obj?.with { } 这种写法。

可空对象可以先判空:

kotlin 复制代码
fun printUser(user: User?) {
    if (user != null) {
        with(user) {
            println(name)
            println(age)
        }
    }
}

也可以使用 ?.run { }

kotlin 复制代码
fun printUser(user: User?) {
    user?.run {
        println(name)
        println(age)
    }
}

with(user) 本身可以接收可空对象,但 Lambda 内的接收者类型也会是可空类型。

kotlin 复制代码
fun printNullableUser(user: User?) {
    with(user) {
        println(this?.name)
    }
}

这段代码能运行,但多数情况下不如先判空或使用 ?.run { } 清楚。

和 apply 的区别

withapply 都使用 this

区别在返回值和调用意图:

函数 调用方式 返回值 常见用途
with with(obj) { } Lambda 结果 集中操作已有对象并返回结果
apply obj.apply { } 对象本身 初始化、配置对象

示例:

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 = with(config) {
        "$host:$port"
    }

    println(address)
}

输出:

text 复制代码
127.0.0.1:8080

apply 返回配置好的对象,with 返回基于这个对象计算出来的字符串。

和 let 的区别

withlet 都能返回 Lambda 结果。

区别在于访问对象的方式。

函数 上下文对象 返回值 常见用途
with this Lambda 结果 集中访问对象成员
let it Lambda 结果 空安全、链式转换、显式参数

示例:

kotlin 复制代码
val text1 = with(user) {
    "$name-$age-$city"
}

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

字段很多时,with 可以少写 it

需要明确参数名时,let 更合适:

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

实战 Demo:生成用户摘要

kotlin 复制代码
data class Order(
    val id: String,
    val amount: Double
)

data class UserSummary(
    val id: Long,
    val name: String,
    val city: String,
    val orders: List<Order>
)

fun buildUserSummary(user: UserSummary): String {
    return with(user) {
        val totalAmount = orders.sumOf { it.amount }
        val orderCount = orders.size

        "用户=$name, 城市=$city, 订单数=$orderCount, 总金额=$totalAmount"
    }
}

fun main() {
    val user = UserSummary(
        id = 1001,
        name = "Tom",
        city = "上海",
        orders = listOf(
            Order("A001", 120.0),
            Order("A002", 260.0)
        )
    )

    println(buildUserSummary(user))
}

输出:

text 复制代码
用户=Tom, 城市=上海, 订单数=2, 总金额=380.0

这里需要多次读取 user 的字段,并返回一段摘要文本,适合使用 with

实战 Demo:矩形面积和周长

kotlin 复制代码
data class Rectangle(
    val width: Int,
    val height: Int
)

data class RectangleInfo(
    val area: Int,
    val perimeter: Int
)

fun calculate(rectangle: Rectangle): RectangleInfo {
    return with(rectangle) {
        RectangleInfo(
            area = width * height,
            perimeter = (width + height) * 2
        )
    }
}

fun main() {
    val info = calculate(Rectangle(width = 5, height = 10))
    println(info)
}

输出:

text 复制代码
RectangleInfo(area=50, perimeter=30)

with(rectangle)widthheight 可以直接参与计算。

实战 Demo:配置已有对象

with 也能修改已有对象。

kotlin 复制代码
data class TextStyle(
    var text: String = "",
    var size: Int = 14,
    var color: String = "black",
    var bold: Boolean = false
)

fun main() {
    val style = TextStyle()

    with(style) {
        text = "Kotlin with"
        size = 18
        color = "blue"
        bold = true
    }

    println(style)
}

输出:

text 复制代码
TextStyle(text=Kotlin with, size=18, color=blue, bold=true)

如果是创建对象时顺手配置,apply 更常见:

kotlin 复制代码
val style = TextStyle().apply {
    text = "Kotlin with"
    size = 18
    color = "blue"
    bold = true
}

如果对象已经存在,with(style) { } 也很自然。

实战 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 buildReportSummary(orders: List<PayOrder>): String {
    val reports = orders
        .filter { it.paid }
        .groupBy { it.city }
        .map { (city, cityOrders) ->
            CityReport(
                city = city,
                orderCount = cityOrders.size,
                totalAmount = cityOrders.sumOf { it.amount }
            )
        }
        .sortedByDescending { it.totalAmount }

    return with(reports) {
        val totalCount = sumOf { it.orderCount }
        val totalAmount = sumOf { it.totalAmount }
        val cityNames = joinToString { it.city }

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

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

输出:

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

reports 已经是一个确定存在的列表,后面需要围绕它计算多个值,with(reports) 能把这段汇总逻辑收在一起。

实战 Demo:用 StringBuilder 生成文本

kotlin 复制代码
data class Article(
    val title: String,
    val author: String,
    val tags: List<String>
)

fun buildMarkdown(article: Article): String {
    return with(StringBuilder()) {
        appendLine("# ${article.title}")
        appendLine()
        appendLine("作者:${article.author}")
        appendLine()
        appendLine("标签:${article.tags.joinToString()}")
        toString()
    }
}

fun main() {
    val article = Article(
        title = "Kotlin with",
        author = "Tom",
        tags = listOf("Kotlin", "Scope Function")
    )

    println(buildMarkdown(article))
}

输出:

text 复制代码
# Kotlin with

作者:Tom

标签:Kotlin, Scope Function

这里的接收者是 StringBuilder,在 with 代码块里可以连续调用 appendLine

最后一行 toString() 是返回值。

实战 Demo:测试代码里的集中操作

下面示例展示测试代码里常见的风格。

kotlin 复制代码
class ShoppingCart {
    private val items = mutableListOf<String>()

    fun add(item: String) {
        items.add(item)
    }

    fun remove(item: String) {
        items.remove(item)
    }

    fun size(): Int {
        return items.size
    }

    fun contains(item: String): Boolean {
        return items.contains(item)
    }
}

fun main() {
    val cart = ShoppingCart()

    val result = with(cart) {
        add("Book")
        add("Pen")
        remove("Pen")

        contains("Book") && size() == 1
    }

    println(result)
}

输出:

text 复制代码
true

with(cart) 把多个购物车操作放在同一个对象作用域里,最后返回断言结果。

嵌套 with 和 this 指向

嵌套 with 时,this 会指向最近一层接收者。

kotlin 复制代码
class Address {
    var city: String = ""
    var street: String = ""
}

class Customer {
    var name: String = ""
    var address: Address = Address()
}

fun main() {
    val customer = Customer()

    with(customer) {
        name = "Tom"

        with(address) {
            city = "上海"
            street = "人民路"
        }
    }

    println(customer.name)
    println(customer.address.city)
}

外层 with(customer) 里,thisCustomer

内层 with(address) 里,thisAddress

如果需要在内层访问外层对象,可以使用标签:

kotlin 复制代码
fun main() {
    val customer = Customer()

    with(customer) customerBlock@{
        name = "Tom"

        with(address) {
            city = "上海"
            street = "${this@customerBlock.name} 的地址"
        }
    }

    println(customer.address.street)
}

输出:

text 复制代码
Tom 的地址

with、run、apply、let、also 的区别

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

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

简单选择方式:

text 复制代码
已有非空对象,集中访问成员并返回结果:with
对象可能为空,需要安全调用:run
需要初始化或配置对象并返回对象本身:apply
需要用显式参数名做转换:let
需要插入日志或附加动作:also

常见注意点

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

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

    val result = with(user) {
        age + 1
    }

    println(result)
}

输出:

text 复制代码
19

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

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

with 不支持 obj?.with 形式

kotlin 复制代码
val user: User? = null

// 错误写法
// user?.with {
//     println(name)
// }

可空对象更常见的写法是:

kotlin 复制代码
user?.run {
    println(name)
}

with 适合已有对象,不适合强行包裹所有逻辑

kotlin 复制代码
val result = with(user) {
    "$name-$age"
}

这种写法很自然。

如果代码块和某个对象关系不强,顶层 run { } 可能更清楚:

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

嵌套 with 需要留意 this

kotlin 复制代码
with(customer) {
    with(address) {
        // this 是 address
    }
}

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

总结

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

  • with 是普通函数,不是扩展函数
  • 调用方式是 with(obj) { }
  • Lambda 内部通过 this 访问传入对象
  • with 返回 Lambda 最后一行结果
  • with 适合对已有非空对象集中操作
  • 可空对象通常使用 ?.run { }
  • 需要返回对象本身时,使用 apply

with 的定位是:把一个已经存在的对象放进作用域里,围绕它完成一段集中操作,然后把结果返回出来。

相关推荐
QING61812 小时前
Kotlin 日常开发常用语法糖整理 —— 速记
android·kotlin·android jetpack
popcorn_min12 小时前
共享单车需求预测:时间特征工程 + 随机森林,R² 达到 0.931
随机森林·r语言·kotlin
Attachment George13 小时前
山东大学软件学院-项目实训-个人开发日志(十):材料问答链路开发——文档解析、OCR兜底与持续追问完善
python·ai·langchain·kotlin·rag
Kapaseker13 小时前
一个丝滑的数字计数器,讲清楚 AnimatedContent 怎么用
android·kotlin
plainGeekDev1 天前
网络状态监听 → ConnectivityManager + Flow
android·java·kotlin
Kapaseker2 天前
你的第一个 Agent — 切换模型
kotlin·agent
JohnnyDeng942 天前
【Android】ViewModelScope 与协程生命周期管理:告别内存泄漏,掌控异步边界
android·kotlin·mvvm·协程
alexhilton3 天前
Android的Agent优先时代:构建时vs运行时
android·kotlin·android jetpack
JohnnyDeng943 天前
【Android】Android 包体积优化:R8/ProGuard 深度配置全攻略
android·性能优化·kotlin·jetpack