当前位置: 首页 > news >正文

Kotlin杂学:让代码优雅的作用域函数

一、 核心概念引入:什么是作用域函数?

1. 概念解析与核心作用

在 Kotlin 中,标准库提供了几个函数,它们的唯一目的是在对象的上下文中执行代码块。当你对一个对象调用这类函数并提供一个 Lambda 表达式时,它会形成一个临时作用域。在这个作用域内,你可以直接访问该对象,而不需要使用它的完整名称。

对比示例:

data class Person(var name: String, var age: Int, var city: String) // 【不使用作用域函数】代码冗余,多次重复输入 person 对象名 val person = Person("Alice", 20, "Amsterdam") person.age = 21 person.city = "London" person.name = "Alice Smith" // 【使用作用域函数 (apply)】代码聚合度高,无需重复输入对象名 val person2 = Person("Alice", 20, "Amsterdam").apply { age = 21 city = "London" name = "Alice Smith" }
2. 底层原理:为什么没有性能开销?

很多人会担心:大量使用这些带有 Lambda 表达式的函数,会不会频繁创建匿名内部类对象,从而拖慢程序性能?答案是:完全不会。因为这五个标准作用域函数在 Kotlin 源码中都被声明为了inline(内联函数)。编译器在编译时,会直接把作用域函数里面的代码“复制粘贴”到调用处。因此,它们在底层几乎等同于普通的顺序执行代码,没有任何额外的内存分配开销。


二、 核心差异解析:如何区分它们?

记住区分它们的两个“黄金维度”,这是死记硬背的解药:

  1. 怎么称呼这个对象(上下文对象)?

    • this(Receiver 接收者):在 Lambda 内部,对象变成了this。你可以直接调用它的属性和方法(省略this.)。适合用来修改或配置对象的属性

    • it(Argument 参数):在 Lambda 内部,对象被作为参数传入,默认名字是it。如果你不想让代码里的方法调用产生歧义,或者需要把对象当作参数传给其他方法,用it最好。

  2. 执行完返回什么?

    • 对象本身 (Context Object):执行完 Lambda 后,把调用者自己原封不动地返回。适合链式调用后续的配置操作。

    • Lambda 结果 (Lambda Result):返回 Lambda 表达式里最后一行的执行结果。适合做计算或类型转换

  • 普通 Lambda 参数block: (T) -> R

    • 含义:传入一个 Lambda,它接收一个类型为T的参数,返回类型为R

    • 指代:在调用时,这个类型为T的参数默认被命名为it。你可以把它重命名(比如user ->)。

  • 带接收者的 Lambdablock: T.() -> R

    • 含义:把 Lambda 作为类型T扩展函数

    • 指代:既然是扩展函数,在 Lambda 内部,你实际上是在对象T的内部编写代码,因此上下文对象变成了this。而且this是可以省略的!


三、 五大作用域函数详解(逐一击破)

3.1let函数
  • 特征:对象是it,返回 Lambda 结果。

  • //let源码解析 public inline fun <T, R> T.let(block: (T) -> R): R { return block(this) }
    • 作用:将调用者(对象T)作为参数传递给 Lambda,并返回 Lambda 的执行结果(R)。

    • it/this指代分析

      • 看参数签名:block: (T) -> R。这是一个普通的 Lambda,接收T作为参数。

      • 因此,let的大括号内,上下文对象通过it指代

      • 看内部实现:return block(this)。它把当前的调用者(源码里的this)传给了block,并直接返回了block的结果。

    • 常用场景 1:非空安全调用 (?.let)

      var name: String? = null // 只有当 name 不为 null 时,才会执行大括号里的代码 name?.let { println("名字的长度是: ${it.length}") }
    • 常用场景 2:变量转换

      val numbers = mutableListOf("one", "two", "three", "four", "five") val resultList = numbers.map { it.length }.filter { it > 3 } // 把上面的计算结果通过 let 打印出来,而不需要声明额外的临时变量 resultList.let { println("符合条件的元素个数是: ${it.size}") }
3.2run函数
  • 特征:对象是this,返回 Lambda 结果。

  • //run源码 public inline fun <T, R> T.run(block: T.() -> R): R { return block() }
    • 作用:在调用者(对象T)的上下文中执行 Lambda,并返回 Lambda 的执行结果(R)。通常用于对象的初始化和计算。

    • it/this指代分析

      • 看参数签名:block: T.() -> R。这是一个带接收者的 Lambda

      • 因此,run的大括号内,上下文对象变成了接收者,通过this指代(可省略)。

      • 看内部实现:return block()。由于blockT的扩展函数,直接调用并返回结果。

    • 常用场景 1:对象配置并计算出结果

      val service = NetworkService() // 配置 service(因为是 this,可以直接调方法),最后返回连接状态 val isConnected = service.run { port = 8080 host = "127.0.0.1" connect() // 返回 Boolean }
    • 常用场景 2:非扩展的 run(单纯执行一个代码块)

      val hexNumberRegex = run { val digits = "0-9" val hexDigits = "A-Fa-f" val sign = "+-" Regex("[$sign]?[$digits$hexDigits]+") }
3.3with函数
  • 特征:作为参数传入对象,对象是this,返回 Lambda 结果。

  • //with源码 public inline fun <T, R> with(receiver: T, block: T.() -> R): R { return receiver.block() }
    • 作用:它不是扩展函数,而是一个普通函数。接收一个对象receiver和一个 Lambda,在receiver的上下文中执行 Lambda,返回结果。常用于对同一个对象进行多次操作。

    • it/this指代分析

      • 看参数签名:block: T.() -> R。同样是带接收者的 Lambda

      • 因此,with的大括号内,传入的receiver对象通过this指代

      • 看内部实现:return receiver.block()。显式地在传入的receiver对象上调用扩展函数block并返回。

    • 常用场景:分组调用某对象的多个方法

      val numbers = mutableListOf("one", "two", "three") // "对于 numbers 这个对象,做以下操作" val firstAndLast = with(numbers) { val firstItem = first() // 相当于 this.first() val lastItem = last() "第一个是 $firstItem, 最后一个是 $lastItem" // 返回这行字符串 } println(firstAndLast)
3.4apply函数
  • 特征:对象是this,返回对象本身。

  • //apply源码 public inline fun <T> T.apply(block: T.() -> Unit): T { block() return this }
    • 作用:在调用者(对象T)的上下文中执行 Lambda,但不关心 Lambda 的返回值,而是直接返回调用者对象本身。这是配置对象最常用的函数。

    • it/this指代分析

      • 看参数签名:block: T.() -> Unit带接收者的 Lambda,且没有返回值(Unit)。

      • 因此,apply的大括号内,上下文对象通过this指代

      • 看内部实现:执行block()后,直接return this(返回了对象自身)。这就是它能实现链式调用的根本原因。

    • 常用场景:对象的初始化与属性配置(最最常用)

      // 在 Android 开发中配置 UI 极其常见 val textView = TextView(context).apply { text = "Hello World" textSize = 20f setOnClickListener { /* do something */ } } // 返回的就是配置好的 textView 实例
3.5also函数
  • 特征:对象是it,返回对象本身。

  • //also源码 public inline fun <T> T.also(block: (T) -> Unit): T { block(this) return this }
    • 作用:将调用者作为参数传递给 Lambda,执行一些附加操作(副作用),最后返回调用者对象本身。

    • it/this指代分析

      • 看参数签名:block: (T) -> Unit。这是一个普通的 Lambda。

      • 因此,also的大括号内,上下文对象通过it指代

      • 看内部实现:执行block(this)(把对象传给 Lambda 执行),然后return this(返回对象自身)。

    • 常用场景:执行副作用(如打印日志)、不破坏链式调用

      val numbers = mutableListOf("one", "two", "three") numbers .also { println("操作前的列表: $it") } // 插入日志,原样返回 numbers .add("four")

      神级应用:交换两个变量(利用 also 返回自身的特性)

      var a = 1 var b = 2 a = b.also { b = a } // 此时 b 变成了 a(1),返回原来的 b(2) 赋值给 a

四、 关联函数扩展学习 (takeIf&takeUnless)

这两个函数可以把if-else的逻辑融入到链式调用中。

  • takeIf:如果满足条件,返回调用者自己;否则返回null

  • takeUnless:如果满足条件,返回调用者自己;否则返回null

代码实例:

val number = 15 // 传统写法 val evenOrNull1 = if (number % 2 == 0) number else null // 使用 takeIf 链式写法 val evenOrNull2 = number.takeIf { it % 2 == 0 } // 配合 ?.let 形成终极连招 File("data.txt") .takeIf { it.exists() } // 存在才返回 File 对象,否则返回 null ?.let { println("文件大小为: ${it.length()}") } // 非空才执行打印

五、 选型指南与对比总结(核心精髓)

这部分你总结的表格已经非常完美,也是日后查阅的利器:

函数对象引用返回值是否为扩展函数核心记忆词(主要用途)
letitLambda 结果非空执行/ 结果转换
runthisLambda 结果初始化并计算
withthisLambda 结果分组调用
applythis对象本身对象配置
alsoit对象本身附加操作(如日志打点)

看完源码,你会发现这五个函数不过是以下两个维度的组合游戏:

  1. 传参方式决定了用什么指代:

    • 源码写了(T) -> ...➡️ 里面就是it(let,also)

    • 源码写了T.() -> ...➡️ 里面就是this(run,with,apply)

  2. 最后一行return决定了返回值:

    • 源码return block()➡️ 返回 Lambda最后一行结果(let,run,with)

    • 源码return this➡️ 返回对象自己(apply,also)


六、 最佳实践与避坑指南

  1. 极力避免“嵌套地狱”千万不要把applyrun嵌套使用。一旦嵌套,内层的this会遮蔽外层的this,读代码的人(包括你自己)很快就会崩溃,不知道哪个方法属于哪个对象。

    • 避坑做法:如果实在要嵌套,优先使用letalso,并把it重命名为有意义的变量名。

  2. thisvsit的选择哲学

    • 如果你在代码块里主要是调用对象的属性或方法来改变对象自身(配置),用this(apply,run,with),代码最省事。

    • 如果你在代码块里主要是把对象作为一个参数传递给其他函数,或者主要是想读取对象的值,用it(let,also),逻辑更清晰。

  3. 优雅地串联链式调用作用域函数的魅力在于组合。比如处理一个新用户注册:

    User("tom@example.com") .apply { age = 25 } // 1. 配置对象 .also { log("创建了新用户: $it") } // 2. 打印日志(附加操作) .takeIf { it.isValid() } // 3. 校验有效性 ?.let { repository.save(it) } // 4. 若有效则保存并返回数据库结果
  4. 可读性优先(最重要的原则)Kotlin 官方强调:不要为了炫技而强行使用作用域函数。如果一个简单的if (person != null)person?.let更好懂,那就用普通的if。代码是写给人看的,顺带让机器执行。

http://www.jsqmd.com/news/536209/

相关文章:

  • Day24:向量数据库 Chroma_FAISS 入门
  • OpenClaw+GLM-4.7-Flash:自动化内容创作全流程
  • 五肽-48——由精氨酸、谷氨酸、亮氨酸、丝氨酸和苏氨酸的抗衰肽
  • 半桥LLC谐振变换器:开环、闭环及闭环+软启动Simulink仿真设计与配套说明文档详解
  • 力扣链表高频题:两两交换节点 + K个一组翻转链表(保姆级思路+满分代码)
  • OpenClaw技能扩展实战:基于百川2-13B-4bits的Markdown周报自动生成
  • 关于Shader学习路上的心得
  • 如何在openKylin下将vsftpd配置成可以让匿名用户访问(v0.2.0)
  • IIC总线
  • 零基础玩转OpenClaw:Qwen3.5-4B-Claude镜像云端体验指南
  • 闲置空间变增收宝地!全自动泡面机免费投放 全国都可以测位置
  • 如何在开放麒麟(openKylin)下安装FTP服务器(v0.2.0)
  • 什么牌子的大路灯护眼好?公认口碑最好的大路灯推荐排行榜前十名
  • 【华为OD机试真题】战场索敌 · 区域统计问题 (Python /JS)
  • 量子赌场黑客:修改概率云薅走十亿
  • 安装 Redis 为系统服务
  • DeerFlow企业级AI研究框架:3种集成模式与扩展架构设计
  • 如何在5分钟内快速部署开源项目:Ultralytics YOLO零基础配置指南
  • PLECS 4.7:虚拟同步机控制三相逆变仿真及报告
  • 密封圈源头厂家提供O型圈定制及国产替代服务:导向带/工程机械密封圈/弹簧蓄能密封圈/旋转密封圈/橡胶密封圈/泛塞封/选择指南 - 优质品牌商家
  • 从0基础到高薪入职:2026大专财富管理专业“三步走”职业规划图
  • 5个开源教育革新角度:释放3D创作工具的教学价值
  • 协议不通?一“网”打尽!PROFINET转MODBUS TCP网关,赋能步科伺服精准协同
  • Conda环境下的WebRTC编译与部署:从源码下载到实战避坑指南
  • WPI Romi 32U4机器人库:嵌入式教育级硬件抽象与PID控制实践
  • Popcorn Time:高效实用的开源跨平台媒体播放解决方案
  • 效率向|小成本做大项目,VP+三易串口屏是秘密武器
  • MATLAB高斯背景建模与目标提取(人体检测)
  • 透明显示屏技术应用:汽车挡风玻璃可直接显示导航信息
  • OpenClaw资源监控:RTX4090D运行Qwen3-32B镜像的优化基线