【日常知识积累】Kotlin let 函数、inline 函数以及 DSL

凡事有交代 ------ 接到任务时,主动明确任务目标、背景、利益相关方、交付物、时间节点。

件件有着落 ------ 执行任务过程中,在里程碑节点及时同步,做好预期管理。

事事有回音 ------ 任务完成后,有复盘,有汇报,有总结。

这篇文章记录了近期积累的一些零散知识点,临近元旦、春节,这个时期是一年里工作相对轻松的阶段,同时也是复盘总结过去一年的经验、以及做好明年目标规划的关键节点。利用好,别虚度

你真的需要使用 let 函数吗?

在 Kotlin 中,letapplyalsorun 等等函数,是降低代码行数、提升可读性的利器,然而它们并不是万能的,如果使用不当,会带来相反的效果。

let 的一般用法

kotlin 复制代码
// 如果元素非空,则对其进行绘制
element?.let { render(it) }

// 非 let 写法
if (element != null) {
	render(element)
}

可以看到,let 函数将3行代码精简成为1行,精简了 66% 的代码。它适用于以下场景:

  • 单次调用该对象
  • lambda 行数很少
  • 没有后续依赖

与之相反的条件,就是不适用于 let 的场景。

let 不适用的场景

kotlin 复制代码
// 灾难现场1:行数多、有后续依赖
element?.let { e ->
    config?.let { c ->
        adapter?.let { a ->
            renderer?.let { r ->
                r.render(e, c, a)
            }
        }
    }
}

// 灾难现场2:超过1行使用 let 对象
element?.let {
    doSomething(it)
    doAnotherThing(it)
    doMore(it)
}

以上已经是 Kotlin 的反模式了,这样的代码不断向右偏移(rightward drift)难读并且难改,在进行 Code Review 时一定要识别出来。

替代方案 return fast

在函数的开头,检查变量条件,如果不符合立刻 return,可以在需要的位置增加日志打印,以便追溯问题。

kotlin 复制代码
fun updateView() {
    val e = element ?: return
    val c = config ?: return
    val a = adapter ?: return

    render(e, c, a)
}

为什么这是好代码?

  • 没有嵌套
  • 控制流清晰
  • 可调试(每一行都能打断点)
  • 非常符合 "fail fast" 原则

更进一步,如果 e、c、a 这三个对象经常一起判空,可以将它们本质上是一个状态,推荐对它们进行状态聚合,有利于长期使用。

kotlin 复制代码
// 状态聚合
data class RenderState(
    val element: Element,
    val config: Config,
    val adapter: Adapter
)

// 只有一个变量
val state: RenderState? = ...

// 使用时判断该变量
fun updateView() {
    val s = state ?: return
    render(s)
}

此外,如果发现,代码里多处进行判空操作,则可以利用 DSL,将判空操作提取出 withReadyElement 函数,

kotlin 复制代码
// 提取 inline 函数,集中进行可空性处理
private inline fun withReadyElement(block: (Element) -> Unit) {
    val e = element ?: return
    block(e)
}

// 调用点更加美观干净
fun updateView() {
    withReadyElement {
        render(it)
    }
}

inline fun 用于消除 Lambda 对象额外开销

inline fun 是"为 Lambda 买单的工具",用于消除 Lambda 参数引起的对象创建&间接调用开销,同样地,它也不是"银弹",如果使用不当,反而会引起性能下降。

普通 Lambda 函数与 inline Lambda 函数

普通 Lambda 函数

kotlin 复制代码
fun doSomething(block: () -> Unit) {
    block()
}

系统在编译这段代码时,实际上发生了:

  • 创建一个 Function0 对象(表示0个参数、即无参函数)
  • 捕获外部变量,生成闭包
  • 通过 invoke() 间接调用

例如,我们调用 doSomething { println("hello") } 后,编译器实际上创建了一个类,并调用其 invoke 函数。

java 复制代码
class DoSomething$1 implements Function0<Unit> {
    @Override
    public Unit invoke() {
        System.out.println("hello")
        return Unit.INSTANCE;
    }
}

inline Lambda 函数

kotlin 复制代码
inline fun doSomething(block: () -> Unit) {
    block()
}

// 编译后直接展开
println("hello")

标准库的 inline 用法

在 Kotlin 标准库里,这些函数都是 inline 实现

  • let
  • apply
  • run
  • with
  • use
  • synchronized

例如:

kotlin 复制代码
inline fun <T> T.apply(block: T.() -> Unit): T

inline 的负面作用

由于 inline 本质上采用了 复制-粘贴 的模式,因此它会在每个调用点复制一份函数代码,这会造成 APK / DEX 膨胀,对 Android 来说,更会引起 方法数增加,甚至突破 64K 限制

inline 的应用场景

  • 参数是 lambda 类型
  • lambda 非常短
  • 该函数 90% 以上是 lambda 调用
  • 应用在循环 / UI / 热路径中
  • inline 收益 > 代码膨胀成本

DSL,把握好"提升可读性"与"滥用"之间的界限

DSL(Domain-Specific Language,领域特定语言),是一种 只为某个特定领域服务 的语言,目标不是"什么都能干",而是把这个领域的表达做到最清晰、最省心、最不容易写错。

如何理解"领域"呢?在我看来,"领域"是指特定的场景、语境、上下文,它具有一定的重复性,因此通过提炼总结 DSL,能够降低重复使用的成本。"领域"可以是技术上的,也可以是业务场景里的。

GPL 与 DSL

与 DSL 相对的概念,是 通用语言(GPL),Java / Kotlin / C++ 都属于此类,它们功能强大且全面,但随之而来的,是写法严谨甚至冗长,无法做到针对特定场景(领域)进行简化。

DSL 则专注于解决一个特定领域的问题,为此进行了特定优化,短、平、快、准。

GPL、DSL 都可以解决问题,重点在于选择使用哪一个。

DSL 的两大类型:外部 DSL

外部 DSL 具备 独立的语法和解析器 ,更像一门新的编程语言,因此其表达能力很强。同时,由于依赖于特定解析器,故定制成本高。

常见的外部 DSL 有:

  • SQL
  • 正则表达式
  • Gradle Groovy/Kotlin DSL(build.gradle)
  • HTML / CSS

DSL 的两大类型:内部 DSL

内部 DSL 在 Kotlin 语言中是非常常见的,例如前文的 letapplyalso 等均属于此类。可以通过 Kotlin 语法特性,使代码"看起来像新的语言"。Kotlin 的标准库中,有很多 API 本身就是 DSL。举两个常见的例子:

例1,Lambda with Receiver

定义 user DSL,代替 set 函数。

kotlin 复制代码
// 使用 Lambda,域内对象为 User
fun user(block: User.() -> Unit) {
    val u = User()
    u.block()
}

// 使用 DSL
user {
	name = "Lei"
}

// 不使用 DSL
user.setName("Lei")

例2, infix 函数

可以提升可读性,例如定义二元运算符 eq,比较 String 对象:

kotlin 复制代码
// 声明 DSL
infix fun String.eq(value: String)

// 使用 DSL
"status" eq "success"

Gradle Kotlin、Jetpack Compose、HTML 都是典型的 DSL,UI结构=代码结构,起到"看代码知UI"的作用。

DSL 不是炫技工具,而是把业务规则提取为代码的手段

值得设计 DSL 不该用DSL
✅是不是反复写相同结构? ✅业务规则是否固定? ✅现在的代码是否"看不出业务语义"? ✅错误是不是经常发生在"用法不对"? ❌一次性逻辑 ❌需求不稳定 ❌团队 Kotlin 水平参差 ❌DSL 设计者不在一线维护
相关推荐
世界美景2 小时前
一种基于 ART 内存特征的 LSPosed/Xposed/分身环境 完美检测方案
android·安全·安卓·xposed
2501_946230983 小时前
Cordova&OpenHarmony外观主题设置
android·javascript
小韩博3 小时前
小迪之盲注第44课
android·网络安全·adb
夏沫琅琊4 小时前
Android TestDPC 工程详解
android
键来大师5 小时前
Android16 AP热点修改默认密码为12345678
android·framework·rk3576·android16
李坤林5 小时前
Android KGI (Generic Kernel Image)
android
十二测试录5 小时前
Android和iOS测试区别
android·经验分享·ios·职场发展·ab测试
柒许宁安5 小时前
在 Cursor 中运行 Android 项目指南
android·java·个人开发
技术小甜甜5 小时前
【Godot】【入门】GDScript 快速上手(只讲游戏里最常用的 20% 语法)
android·游戏·编辑器·游戏引擎·godot