编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

    • Kotlin精通

      • Kotlin精通模块总览
      • Kotlin基础语法
      • 类和对象
      • 高阶特性入门
        • 3.1 扩展函数与属性
          • 3.1.1 扩展函数语法
          • 3.1.2 静态派发本质
          • 3.1.3 扩展属性用法
          • 3.1.4 综合案例与思考
        • 3.2 Lambda与函数式
          • 3.2.1 函数类型语法
          • 3.2.2 高阶函数概念
          • 3.2.3 inline函数意义
          • 3.2.4 综合案例与思考
        • 3.3 作用域函数深潜
          • 3.3.1 五兄弟对比
          • 3.3.2 接收者与参数
          • 3.3.3 场景决策表
          • 3.3.4 综合案例与思考
        • 3.4 空安全实战技巧
          • 3.4.1 let链式空安全
          • 3.4.2 takeIf与takeUnless
          • 3.4.3 契约函数requireNotNull
          • 3.4.4 综合案例与思考
        • 3.5 DSL构建能力
          • 3.5.1 DSL三大支柱
          • 3.5.2 minihtmlDSL
          • 3.5.3 DslMarker防作用域穿透
          • 3.5.4 综合案例与思考
        • 3.6 协程初识
          • 3.6.1 suspend关键字
          • 3.6.2 launch与async
          • 3.6.3 Dispatchers调度器
          • 3.6.4 结构化并发第一课
          • 3.6.5 综合案例与思考
        • 3.7 与Java互操作
          • 3.7.1 JvmStatic与JvmField
          • 3.7.2 JvmOverloads
          • 3.7.3 平台类型陷阱
          • 3.7.4 SAM转换
          • 3.7.5 综合案例与思考
        • 3.8 综合案例练习
          • 3.8.1 网络请求实战
          • 3.8.2 UI状态DSL
        • 3.9 训练题三道
        • 3.10 新手陷阱Top5
      • 空安全类型原理
      • data类与equals原理
      • 协程调度器原理
      • inline与reified原理
      • 委托与属性原理
    • 库的解读

    • 专栏博客

  • iOS开发和进阶

  • Web开发和进阶

  • Linux应用开发

  • IoT智能硬件开发

  • Apps
  • Android提升进阶
  • Kotlin精通
杨充
2026-07-03
目录

高阶特性入门

# 第 3 章 Kotlin 高阶特性入门

# 目录介绍

  • 3.1 扩展函数与属性
    • 3.1.1 扩展函数语法
    • 3.1.2 静态派发本质
    • 3.1.3 扩展属性用法
    • 3.1.4 综合案例与思考
  • 3.2 Lambda与函数式
    • 3.2.1 函数类型语法
    • 3.2.2 高阶函数概念
    • 3.2.3 inline函数意义
    • 3.2.4 综合案例与思考
  • 3.3 作用域函数深潜
    • 3.3.1 五兄弟对比
    • 3.3.2 接收者与参数
    • 3.3.3 场景决策表
    • 3.3.4 综合案例与思考
  • 3.4 空安全实战技巧
    • 3.4.1 let链式空安全
    • 3.4.2 takeIf与takeUnless
    • 3.4.3 契约函数requireNotNull
    • 3.4.4 综合案例与思考
  • 3.5 DSL构建能力
    • 3.5.1 DSL三大支柱
    • 3.5.2 mini_html_DSL
    • 3.5.3 DslMarker防作用域穿透
    • 3.5.4 综合案例与思考
  • 3.6 协程初识
    • 3.6.1 suspend关键字
    • 3.6.2 launch与async
    • 3.6.3 Dispatchers调度器
    • 3.6.4 结构化并发第一课
    • 3.6.5 综合案例与思考
  • 3.7 与Java互操作
    • 3.7.1 JvmStatic与JvmField
    • 3.7.2 JvmOverloads
    • 3.7.3 平台类型陷阱
    • 3.7.4 SAM转换
    • 3.7.5 综合案例与思考
  • 3.8 综合案例练习
    • 3.8.1 网络请求实战
    • 3.8.2 UI状态DSL
  • 3.9 训练题三道
  • 3.10 新手陷阱Top5

# 3.1 扩展函数与属性

# 3.1.1 扩展函数语法

Kotlin 允许你 不修改类本身,就给一个已存在的类型「加上一个方法」——这类方法叫 扩展函数:

fun String.lastChar(): Char = this[length - 1]

fun main() {
    println("hello".lastChar())     // 'o'
}

语法要点:

  • fun 接收者类型.函数名(...):接收者写在函数名前,用 . 分隔
  • 函数体内 this 指向接收者实例(可省略)
  • 调用方看起来跟成员方法 完全一样

看起来像给 String 加了个方法,实际上是 静态函数 + this 参数 的语法糖。

典型使用场景:

// 场景 1:给标准库补充缺失的工具方法
fun <T> List<T>.second(): T = this[1]

// 场景 2:给第三方库/系统 SDK 加语法糖(例如 Android View)
fun View.dp(value: Int) = (value * resources.displayMetrics.density).toInt()

// 场景 3:让接收者对象成为「主语」,读起来更接近自然语言
fun String.isEmail() = matches(Regex("""[\w.]+@[\w.]+"""))

# 3.1.2 静态派发本质

关键区分:扩展函数是 静态派发(按变量声明类型),不是虚方法(按运行时类型):

open class Animal
class Dog : Animal()

fun Animal.name() = "Animal"
fun Dog.name() = "Dog"

fun main() {
    val a: Animal = Dog()
    println(a.name())     // 输出 "Animal"——按声明类型分发!
    val d: Dog = Dog()
    println(d.name())     // 输出 "Dog"
}

关键结论:扩展函数 不参与多态——它由调用点变量的静态类型决定调哪一个。

反编译看真相——fun String.lastChar() 编译后的 Java 等价代码:

public final class StringExtKt {
    public static final char lastChar(String $this) {   // 静态函数
        return $this.charAt($this.length() - 1);
    }
}

// 调用点:
StringExtKt.lastChar("hello");

从字节码看,扩展函数就是一个「把接收者当第一个静态参数」的普通静态函数。这也解释了为什么它不能被 override——静态函数没有虚表。

同名冲突规则:如果扩展函数和成员函数签名相同,成员函数优先:

class Foo {
    fun bar() = "member"
}
fun Foo.bar() = "ext"     // 永远不会被调用

fun main() {
    println(Foo().bar())      // 输出: member
}

编译器会给出警告 "extension is shadowed by a member"。

# 3.1.3 扩展属性用法

扩展属性长这样:

val String.lastChar: Char
    get() = this[length - 1]

fun main() {
    println("hello".lastChar)     // 'o'
}

限制:扩展属性不能有 backing field,只能有 getter/setter。原因显而易见——你不能在一个已经编译好的类里塞一个字段进去。

只读扩展属性用 val + get(),读写扩展属性用 var + get()/set():

// 读写扩展属性——把 View 的 padding 简化成 View.paddingHor
var View.paddingHor: Int
    get() = paddingLeft
    set(value) { setPadding(value, paddingTop, value, paddingBottom) }

# 3.1.4 综合案例与思考

Android View 常用扩展——工程实战中每一个 Kotlin Android 项目都会写的一套:

import android.view.View
import android.content.Context

// 尺寸
val Int.dp: Int get() = (this * resources.displayMetrics.density).toInt()
val Int.px: Float get() = this.toFloat() / resources.displayMetrics.density

// 可见性
fun View.visible() { visibility = View.VISIBLE }
fun View.gone() { visibility = View.GONE }
fun View.invisible() { visibility = View.INVISIBLE }

// 条件可见
fun View.showIf(cond: Boolean) { visibility = if (cond) View.VISIBLE else View.GONE }

// 防重复点击
fun View.setSafeClickListener(intervalMs: Long = 500, block: (View) -> Unit) {
    var lastClick = 0L
    setOnClickListener {
        val now = System.currentTimeMillis()
        if (now - lastClick >= intervalMs) {
            lastClick = now
            block(it)
        }
    }
}

调用侧代码从「一大堆样板」变成「一行链式」:

// 之前
button.setOnClickListener { doSomething() }
button.visibility = View.GONE

// 之后
button.setSafeClickListener { doSomething() }
button.gone()

思考:扩展函数能「覆盖」成员函数吗?如果同名冲突会怎样?

答案:不能覆盖。同名冲突时成员函数永远赢,编译器会警告 "extension is shadowed by a member"。扩展函数本质是静态函数,不参与 Kotlin 的虚方法分发机制。


# 3.2 Lambda与函数式

# 3.2.1 函数类型语法

Kotlin 把「函数」视为一等公民,可以被赋值、传参、作为返回值:

val add: (Int, Int) -> Int = { a, b -> a + b }
val greet: () -> Unit = { println("Hi") }
val withReceiver: String.() -> Int = { length }       // 带接收者的 Lambda

fun main() {
    println(add(3, 5))                    // 8
    greet()                                // Hi
    println("hello".withReceiver())        // 5,接收者是 "hello"
}

函数类型语法总览:

语法 含义
() -> Unit 无参、无返回
(Int) -> Int 一个 Int 参数,返回 Int
(Int, String) -> Boolean 两参多返
String.() -> Int 带接收者的 Lambda,内部 this 是 String
suspend () -> Unit 挂起函数类型
(Int) -> Unit? 返回可空的 Unit

核心:(A, B) -> R 是一等类型,本质上是接口 Function2<A, B, R>——Kotlin 标准库定义了 Function0 ~ Function22,最多支持 22 个参数的 Lambda。

# 3.2.2 高阶函数概念

高阶函数 = 接受或返回函数的函数。

// 接受函数作为参数
fun <T, R> Iterable<T>.myMap(transform: (T) -> R): List<R> {
    val result = mutableListOf<R>()
    for (item in this) result.add(transform(item))
    return result
}

// 返回函数
fun multiplier(factor: Int): (Int) -> Int = { x -> x * factor }

fun main() {
    val doubled = listOf(1, 2, 3).myMap { it * 2 }
    println(doubled)                       // [2, 4, 6]

    val timesTen = multiplier(10)
    println(timesTen(7))                   // 70
}

标准库大量使用高阶函数:map / filter / fold / reduce / groupBy / sortedBy ... 这套 API 让集合处理彻底摆脱 for 循环。

# 3.2.3 inline函数意义

高阶函数每次调用都会 创建一个 Lambda 对象——如果在热点循环里,可能带来大量分配:

fun <T> List<T>.myFilter(p: (T) -> Boolean): List<T> {
    // 每次调用 myFilter 都会 new 一个 Function1 实例传给 p
    ...
}

inline 让高阶函数在 调用点代码展开,Lambda 不再是对象分配:

inline fun <T> Iterable<T>.myFilter(predicate: (T) -> Boolean): List<T> {
    val result = mutableListOf<T>()
    for (item in this) if (predicate(item)) result.add(item)
    return result
}

用户代码:

val evens = listOf(1, 2, 3, 4).myFilter { it % 2 == 0 }

编译后(大致):

// 相当于把 myFilter 的方法体和 Lambda 都展开到调用点
val result = mutableListOf<Int>()
for (item in listOf(1, 2, 3, 4)) if (item % 2 == 0) result.add(item)
val evens = result

没有 Lambda 对象分配、没有虚方法调用——性能等同于手写循环。

字节码原理见 原理篇 4。

用法准则:

  • ✅ 接受 Lambda 参数的高阶函数——建议 inline(性能收益大)
  • ❌ 不接受 Lambda 的函数——不要 inline(膨胀无收益)
  • ❌ 大函数体的 inline——慎重(大量调用点会导致字节码急剧膨胀)
  • ✅ 需要 reified 泛型的函数——必须 inline

# 3.2.4 综合案例与思考

通用 retry 函数——inline + Lambda 组合:

inline fun <T> retry(
    times: Int = 3,
    delayMs: Long = 500,
    block: (Int) -> T
): T {
    var last: Throwable? = null
    repeat(times) { attempt ->
        try {
            return block(attempt)
        } catch (e: Throwable) {
            last = e
            if (attempt < times - 1) Thread.sleep(delayMs)
        }
    }
    throw last!!
}

fun main() {
    val result = retry(times = 3) { attempt ->
        println("尝试 #$attempt")
        if (attempt < 2) throw RuntimeException("失败")
        "成功"
    }
    println(result)
}

输出:

尝试 #0
尝试 #1
尝试 #2
成功

思考:Kotlin 的 Lambda 与 Java 的 Function<T, R>(java.util.function)如何互相转换?

答案:

  • Java → Kotlin:Java 的 Function<T, R> 可以直接当 Kotlin 的 (T) -> R 用(Kotlin 有 SAM 转换支持)
  • Kotlin → Java:Kotlin Lambda 编译后是 Function1 类型,Java 侧调用需要 .apply() 方法(不是 Java Function.apply,而是 Function1.invoke)——建议给 Kotlin 函数加 @JvmName 或改用 fun interface

# 3.3 作用域函数深潜

let / run / with / apply / also 是 Kotlin 特有的「作用域函数」,都是把一个对象包在 Lambda 作用域里访问。他们的差别在两个维度:接收者引用方式 和 返回值。

# 3.3.1 五兄弟对比

函数 接收者引用 返回值 典型场景
let it Lambda 结果 空安全链、局部改名
run this Lambda 结果 对象配置 + 计算
with this Lambda 结果 非扩展写法的 run
apply this 接收者本身 对象初始化建造
also it 接收者本身 副作用(打印、日志)

一张图看清区别:

                 引用方式
                this     it
              ┌──────┬──────┐
返回 Lambda   │ run  │ let  │
返回接收者    │apply │ also │
              └──────┴──────┘
       with = run 的非扩展版本

# 3.3.2 接收者与参数

核心区分:

  • this 接收者:Lambda 内可以直接调用成员(this. 可省略),像在类内部写代码
  • it 参数:Lambda 内像访问普通变量(可改名为其他),显式
data class Config(var debug: Boolean = false, var timeout: Int = 0)

// this 风格(apply)
val ok = Config().apply {
    debug = true                // 直接访问成员,等价于 this.debug
    timeout = 30
}

// it 风格(let)
val length = "hello".let {
    println(it)                  // 通过 it 访问
    it.length
}

// 也可以给 it 改名
val length2 = "hello".let { str ->
    println(str)
    str.length
}

选择 this 风格的时机:调用大量成员方法/属性时——省略 this. 让代码更简洁。

选择 it 风格的时机:Lambda 内需要多次引用外层作用域,避免 this 混淆;或需要给参数起个业务名。

# 3.3.3 场景决策表

需要返回接收者本身吗?
├─ 是:需要用 this 简洁访问成员?
│    ├─ 是 → apply       // 对象建造:Config().apply { debug = true }
│    └─ 否 → also        // 副作用:response.also { log(it) }
└─ 否:需要用 this 简洁访问成员?
     ├─ 是 → run(或 with)  // 计算:config.run { timeout * 1000 }
     └─ 否 → let          // 空安全链:user?.let { fetch(it.id) }

一个真实业务场景多种写法对比:

data class User(var name: String, var age: Int)

// 场景:创建 User,同时打日志,返回 User

// 方案 A:apply(返回接收者,this 引用)
val u1 = User("K", 20).apply {
    println("created: $name")     // 直接访问 name
}

// 方案 B:also(返回接收者,it 引用)
val u2 = User("K", 20).also {
    println("created: ${it.name}")  // 通过 it
}

// 方案 C:let(返回 Lambda 结果,it 引用)——不合适!返回值成了 Unit
val u3 = User("K", 20).let {
    println("created: ${it.name}")   // Unit
}

规则:想「返回对象本身 + 顺便干点事」——apply / also;想「用对象计算个新东西」——let / run。

# 3.3.4 综合案例与思考

五种作用域函数的等价改写练习:

data class HttpConfig(var url: String = "", var timeout: Int = 0)

// 需求:创建一个 HttpConfig,url 设为 "https://x.com",timeout 设 30,然后打印它,最后返回它

// 1. apply(推荐用法)
val a = HttpConfig().apply {
    url = "https://x.com"
    timeout = 30
}.also { println(it) }

// 2. also 单独用
val b = HttpConfig().also {
    it.url = "https://x.com"
    it.timeout = 30
    println(it)
}

// 3. run(返回值我们不需要,等同于 apply)
val c = HttpConfig().run {
    url = "https://x.com"
    timeout = 30
    this
}.also { println(it) }

// 4. with(非扩展写法)
val d = HttpConfig()
with(d) {
    url = "https://x.com"
    timeout = 30
    println(this)
}

// 5. let(不推荐——需要返回自己)
val e = HttpConfig().let {
    it.url = "https://x.com"
    it.timeout = 30
    println(it)
    it
}

思考:apply 与 Java 的 Builder 模式有何异同?

对比:

维度 Java Builder Kotlin apply
需要额外定义 Builder 类 是 否
支持链式调用 是 是(返回 this)
编译产物代码量 每个 Builder ~30 行样板 0
编译期字段安全检查 Builder 属性可能忘赋值 若字段有默认值可直接跳过;若无默认值必须构造时给
复用现有类的 setter 需要一起定义 直接复用

# 3.4 空安全实战技巧

# 3.4.1 let链式空安全

user?.address?.city?.let { println("city=$it") }

只有 user、address、city 都非 null 时才执行 Lambda。这是 Kotlin 空安全的经典模式——把「非空后处理」和「访问路径」在同一行表达。

对比 Java 的等价代码:

if (user != null) {
    Address addr = user.getAddress();
    if (addr != null) {
        String city = addr.getCity();
        if (city != null) {
            System.out.println("city=" + city);
        }
    }
}

更进阶的用法——链式转换:

val cityUpper: String? = user
    ?.address                 // 可空
    ?.city                    // 可空
    ?.let { it.uppercase() }  // 只在非空时转大写
    ?.let { "[$it]" }         // 加括号

# 3.4.2 takeIf与takeUnless

takeIf { } 让接收者对象「按条件通过」——满足条件返回自己,否则返回 null:

val positive = x.takeIf { it > 0 }         // 满足则返回自身,否则 null
val nonBlank = str.takeUnless { it.isBlank() }

典型模式——「验证 + 使用」一体化:

fun findUser(idInput: String): User? = idInput
    .takeIf { it.matches(Regex("\\d+")) }     // 必须全数字
    ?.toLong()
    ?.let { repo.find(it) }

takeIf vs filter:

  • takeIf 作用于 单个对象——通过返回自己,不通过返回 null
  • filter 作用于 集合——过滤出满足条件的元素

# 3.4.3 契约函数requireNotNull

fun send(msg: String?) {
    val notNull = requireNotNull(msg) { "msg 不能为 null" }
    // 从这里开始 notNull 已经被编译器智能转换为 String(非空)
    println(notNull.length)
}

关键:requireNotNull / checkNotNull 用 Kotlin contract 机制 让编译器识别为空断言。

三兄弟对比:

函数 断言失败抛出 语义
requireNotNull(x) IllegalArgumentException 校验函数参数
checkNotNull(x) IllegalStateException 校验对象内部状态
x!! NullPointerException 无消息,仅用于表达式内

推荐姿势:函数入口校验用 requireNotNull(有清晰错误提示),中途表达式用 ?.let 或安全转换,只在「绝无可能为空」时才用 !!。

# 3.4.4 综合案例与思考

处理 JSON 深层嵌套字段——?.let 链 vs 序列化库对比:

// 假设从 JSON 反序列化得到:
// { "user": { "profile": { "avatar": { "url": "..." } } } }

data class Response(val user: User?)
data class User(val profile: Profile?)
data class Profile(val avatar: Avatar?)
data class Avatar(val url: String?)

// ============ 方案 A:手动 ?. 链 ============
fun getAvatar(r: Response): String {
    return r.user?.profile?.avatar?.url ?: ""
}

// ============ 方案 B:?. + let 链,加处理 ============
fun getAvatarBig(r: Response): String? =
    r.user
        ?.profile
        ?.avatar
        ?.url
        ?.let { "$it?size=big" }        // 拼接尺寸参数

// ============ 方案 C:非空则日志 + 使用 ============
fun logAndLoadAvatar(r: Response) {
    r.user?.profile?.avatar?.url
        ?.also { println("[LOG] load $it") }
        ?.let { load(it) }
        ?: println("[LOG] no avatar")
}

思考:?.let 与 if (x != null) 何时选哪个?

答案:

  • if (x != null) 更适合:修改本地变量、大段代码块、x 是本地 val(不涉及并发 smart cast 失效)
  • ?.let 更适合:x 是可空 var 或属性(if 无法 smart cast)、需要链式表达、想给 x 起个不同的名字

典型陷阱:var x 或类属性做 if (x != null) x.length——如果多线程可能失效,编译器要求写 x?.length 或先 val local = x。


# 3.5 DSL构建能力

DSL(Domain Specific Language,领域特定语言)——用 Kotlin 语法描述特定领域的问题,读起来接近自然语言。Compose、Gradle KTS、Ktor、Anko 都是典型 Kotlin DSL。

# 3.5.1 DSL三大支柱

Kotlin DSL 靠 3 个语法支柱:

  1. 尾随 Lambda:函数最后一个参数是 Lambda 时,可以写在圆括号外,甚至直接省略括号
  2. 带接收者的 Lambda:T.() -> Unit 类型的 Lambda 内 this 指向 T 实例
  3. 中缀函数:infix fun a.foo(b: B) → 调用写成 a foo b(省略点和括号)

三支柱结合的例子:

// 支柱 1:尾随 Lambda
fun repeat(n: Int, block: () -> Unit) { for (i in 0 until n) block() }
repeat(3) { println("hi") }             // 括号外的 Lambda

// 支柱 2:带接收者的 Lambda
class Builder { var name = "" }
fun build(block: Builder.() -> Unit) = Builder().apply(block)
build { name = "K" }                    // Lambda 内 this = Builder

// 支柱 3:中缀函数
infix fun Int.pow(n: Int) = Math.pow(this.toDouble(), n.toDouble()).toInt()
println(2 pow 8)                        // 256

# 3.5.2 mini_html_DSL

一个完整可运行的 mini-HTML 生成器:

class Element(val name: String) {
    private val children = mutableListOf<Element>()
    private val attrs = mutableMapOf<String, String>()
    var text: String? = null

    fun attr(key: String, value: String) { attrs[key] = value }

    fun child(name: String, block: Element.() -> Unit) {
        children += Element(name).apply(block)
    }

    override fun toString(): String {
        val attrStr = attrs.entries.joinToString(" ") { """${it.key}="${it.value}"""" }
        val head = if (attrStr.isEmpty()) name else "$name $attrStr"
        val body = if (text != null) text else children.joinToString("")
        return "<$head>$body</$name>"
    }
}

fun html(block: Element.() -> Unit) = Element("html").apply(block)
fun Element.body(block: Element.() -> Unit) { child("body", block) }
fun Element.h1(block: Element.() -> Unit)   { child("h1", block) }
fun Element.p(block: Element.() -> Unit)    { child("p", block) }

fun main() {
    val page = html {
        body {
            h1 { text = "Kotlin DSL" }
            p {
                attr("class", "note")
                text = "Hello from DSL"
            }
        }
    }
    println(page)
}

输出:

<html><body><h1>Kotlin DSL</h1><p class="note">Hello from DSL</p></body></html>

这就是 DSL 的威力——不需要 XML 解析器、不需要模板引擎,用 纯 Kotlin 语法 描述层级化结构。Compose UI、Gradle KTS 都是这个套路。

# 3.5.3 DslMarker防作用域穿透

嵌套 DSL 的作用域穿透问题:

html {
    body {
        h1 {
            text = "标题"
            body { }         // ❌ 危险:语义是「在 h1 里加个 body」——但 body 是 html 的方法!
        }
    }
}

Kotlin 允许 Lambda 内部访问外层作用域的成员——正常场景很方便,但 DSL 里这会导致「作用域穿透」的错误。解决方案:@DslMarker 注解。

@DslMarker
annotation class HtmlDsl

@HtmlDsl
class Element(...)             // 所有 Element 类的实例受同一个 DslMarker 保护

标注后,内层 Lambda 只能访问最近的接收者——尝试穿透到外层会 编译报错:

h1 {
    body { }        // ❌ 编译错:'body' 属于外层 receiver,被 @DslMarker 阻断
}

# 3.5.4 综合案例与思考

Retrofit 风格的接口 DSL:

class Api {
    private val routes = mutableListOf<Route>()

    fun get(path: String, block: Route.() -> Unit) {
        routes += Route("GET", path).apply(block)
    }
    fun post(path: String, block: Route.() -> Unit) {
        routes += Route("POST", path).apply(block)
    }

    class Route(val method: String, val path: String) {
        var handler: ((Map<String, String>) -> String)? = null
        fun handle(h: (Map<String, String>) -> String) { handler = h }
    }

    override fun toString() = routes.joinToString("\n") { "${it.method} ${it.path}" }
}

fun api(block: Api.() -> Unit) = Api().apply(block)

fun main() {
    val a = api {
        get("/users") {
            handle { params -> "list all users" }
        }
        post("/users") {
            handle { params -> "create user: ${params["name"]}" }
        }
    }
    println(a)
}

思考:Gradle Kotlin DSL、Compose UI 都是怎么用这套机制的?

答案:

  • Gradle KTS:plugins { ... } / android { ... } / dependencies { ... } 全部是「带接收者的 Lambda + 尾随 Lambda」
  • Compose UI:@Composable 函数就是「带 Composer 隐式接收者的函数」,Column { ... } 里可以调 Text(...) 因为 Text 是 Composable 顶级函数

# 3.6 协程初识

协程是 Kotlin 官方推荐的并发模型——在语法上像同步代码,运行时却是异步的。本节只做入门,深入原理见 原理篇 3。

# 3.6.1 suspend关键字

suspend fun fetchUser(id: Long): User {
    delay(100)                    // 挂起 100ms 但不阻塞线程
    return User(id, "K")
}

suspend 表示「可挂起」——不占用线程即可等待。挂起点由 delay / withContext / await 等触发。

规则:suspend fun 只能在另一个 suspend fun 或 协程构建器(launch / async / runBlocking)里调用。

为什么需要 suspend——传统回调地狱 vs 协程对比:

// 传统回调
fetchUser(1L) { user ->
    fetchOrders(user.id) { orders ->
        fetchDetail(orders[0].id) { detail ->
            println(detail)
        }
    }
}

// 协程
suspend fun run() {
    val user = fetchUser(1L)
    val orders = fetchOrders(user.id)
    val detail = fetchDetail(orders[0].id)
    println(detail)
}

字节码原理见 原理篇 3。

# 3.6.2 launch与async

构建器 返回 用途
launch Job 启动一个「发射后不管」的协程
async Deferred<T> 启动一个「要拿返回值」的协程,用 .await() 获取
import kotlinx.coroutines.*

fun main() = runBlocking {
    // launch:发射后不管,可用 job.join() 等待
    val job = launch {
        delay(100)
        println("launch done")
    }

    // async:要返回值
    val deferred = async {
        delay(200)
        "async result"
    }

    job.join()
    println(deferred.await())
}

输出:

launch done
async result

# 3.6.3 Dispatchers调度器

调度器 场景
Dispatchers.Main UI 线程(Android)
Dispatchers.IO 网络/文件 IO 密集
Dispatchers.Default CPU 密集计算
Dispatchers.Unconfined 不切线程(很少用)
suspend fun loadData() {
    val data = withContext(Dispatchers.IO) {
        httpFetch()                    // 在 IO 线程池执行
    }
    // 回到调用者所在的 Dispatcher(比如 Main)
    updateUI(data)
}

withContext vs async:withContext 用于 顺序等待,async 用于 并发。想并发拉两个接口:

suspend fun loadAll() = coroutineScope {
    val a = async(Dispatchers.IO) { fetchA() }
    val b = async(Dispatchers.IO) { fetchB() }
    a.await() + b.await()               // 两者并发
}

# 3.6.4 结构化并发第一课

suspend fun loadCombined() = coroutineScope {
    val a = async { fetchA() }
    val b = async { fetchB() }
    a.await() + b.await()          // 出块时保证所有子协程完成或取消
}

核心原则:每个协程都必须有一个明确的 scope,避免 GlobalScope 泄漏。

  • coroutineScope { ... }:任一失败 → 全部取消,抛异常给外层
  • supervisorScope { ... }:子失败不影响其他子,只抛给自己

Android 里的典型 Scope:

class MyViewModel : ViewModel() {
    fun load() {
        viewModelScope.launch {              // ViewModel 销毁时自动 cancel
            val data = withContext(Dispatchers.IO) { fetch() }
            updateState(data)
        }
    }
}

viewModelScope 由 AndroidX 提供,绑定 ViewModel 生命周期——ViewModel 销毁时所有子协程自动取消,避免内存泄漏。

# 3.6.5 综合案例与思考

并发拉取多接口 + 超时 + 取消:

import kotlinx.coroutines.*

suspend fun fetchUser(id: Long): String {
    delay(200); return "user#$id"
}
suspend fun fetchOrders(id: Long): List<String> {
    delay(500); return listOf("order-1", "order-2")
}
suspend fun fetchNotifications(id: Long): List<String> {
    delay(300); return listOf("notif-1")
}

suspend fun homePage(userId: Long): Triple<String, List<String>, List<String>> =
    withTimeout(1_000) {                           // 整体最多 1s
        coroutineScope {
            val user = async { fetchUser(userId) }
            val orders = async { fetchOrders(userId) }
            val notifs = async { fetchNotifications(userId) }
            Triple(user.await(), orders.await(), notifs.await())
        }
    }

fun main() = runBlocking {
    val (u, o, n) = homePage(1L)
    println("user=$u")
    println("orders=$o")
    println("notifs=$n")
}

输出(三个接口并发,总耗时约 500ms 而非 1000ms):

user=user#1
orders=[order-1, order-2]
notifs=[notif-1]

思考:为什么 runBlocking 只应出现在 main 或测试里,不能在业务代码里?

答案:runBlocking 会 阻塞当前线程 直到协程完成——这违背了协程「不阻塞线程」的初衷。在业务代码(尤其 UI 线程)里调 runBlocking = ANR 死锁风险。只有在 main 入口(没有协程 scope 的地方)或者测试代码里,需要「桥接同步和协程」时才用。


# 3.7 与Java互操作

Kotlin 编译到 JVM 字节码,可以无缝调用 Java——但在「Kotlin 类被 Java 调用」的方向上有一些陷阱和优化开关。

# 3.7.1 JvmStatic与JvmField

class Api {
    companion object {
        @JvmStatic
        fun of() = Api()

        @JvmField
        val TAG = "Api"

        const val VERSION = "1.0"     // const 隐式相当于 @JvmField + 编译期常量
    }
}

Java 调用效果:

Kotlin 注解 Java 调用
无标注 Api.Companion.of()、Api.Companion.getTAG()
@JvmStatic 方法 Api.of()
@JvmField 字段 Api.TAG(直接访问字段)
const val Api.VERSION(编译期常量,可用作 case 标签)

建议:SDK 类的 companion 成员统统标注,让 Java 用户能用「像 Java static 一样」的语法访问。

# 3.7.2 JvmOverloads

Kotlin 的 默认参数 在 Java 侧看不到——Java 调用只能全传:

class HttpClient(val timeout: Int = 30, val retry: Int = 3)

Java 侧:new HttpClient(30, 3)——必须全传。加上 @JvmOverloads:

class HttpClient @JvmOverloads constructor(
    val timeout: Int = 30,
    val retry: Int = 3
)

编译器 生成多个重载构造:

HttpClient(int timeout, int retry)     // 全参
HttpClient(int timeout)                // 用默认 retry
HttpClient()                            // 用默认 timeout 和 retry

Java 侧就能享受「省参数」的语法。同样规则适用于 fun:

@JvmOverloads
fun greet(msg: String = "Hi", tag: String = "APP") { }

# 3.7.3 平台类型陷阱

Java 侧的 String 传给 Kotlin,类型被标为 String!——既可能非空也可能可空,Kotlin 不再帮你做静态检查:

// Java 侧
public class JavaApi {
    public String getName() { return null; }    // 可能返回 null
}
// Kotlin 侧
val name = JavaApi().getName()      // 类型是 String!(平台类型)
println(name.length)                 // 编译通过,运行时 NPE

解决方案:

  1. Java 侧加 @Nullable / @NotNull 注解(JetBrains、Android、JSR-305 都可以):
public class JavaApi {
    @Nullable
    public String getName() { return null; }
}

Kotlin 就会正确识别为 String?,编译期就能拦截 NPE。

  1. Kotlin 侧显式转成可空:
val name: String? = JavaApi().getName()
println(name?.length)
  1. 在类边界(数据反序列化入口)主动做非空断言:
val safeName = requireNotNull(JavaApi().getName()) { "name 必须存在" }

# 3.7.4 SAM转换

Java 单方法接口(如 Runnable、Comparator)可用 Lambda 直接构造:

Thread { println("run") }.start()               // 传 Lambda 就是 Runnable
val cmp = Comparator<Int> { a, b -> a - b }

注意:Kotlin 接口自身默认不支持 SAM:

interface OnClick {
    fun onClick()
}

// ❌ Kotlin 1.3 不允许
button.setOnClick { }

Kotlin 1.4+ 提供 fun interface 关键字,声明后就支持 Lambda:

fun interface OnClick {
    fun onClick()
}

button.setOnClick { }              // ✅

# 3.7.5 综合案例与思考

Kotlin SDK 对 Java 使用方友好的清单:

// ✅ 全部 companion 成员加 @JvmStatic / @JvmField
class NetSdk private constructor(config: Config) {
    companion object {
        @JvmStatic
        fun create(config: Config) = NetSdk(config)

        @JvmField
        val DEFAULT_TIMEOUT = 30

        const val VERSION = "1.0"
    }

    // ✅ 有默认参数的构造/方法加 @JvmOverloads
    @JvmOverloads
    fun request(url: String, timeout: Int = DEFAULT_TIMEOUT, retry: Int = 3) { }

    // ✅ 想让 Java 直接看到 property 而非 getter/setter,用 @JvmField
    @JvmField
    var debug = false

    // ✅ 单方法接口用 fun interface
    fun interface Callback {
        fun onResponse(json: String)
    }

    fun requestAsync(url: String, cb: Callback) { }

    // ✅ 抛出 checked 异常要加 @Throws(Kotlin 无 checked exception)
    @Throws(java.io.IOException::class)
    fun readFile(path: String): ByteArray = TODO()

    // ✅ 给顶层函数/类文件加 @file:JvmName 让 Java 调起来自然
}

思考:为什么 Kotlin 类的属性在 Java 侧调用时会看到 getXxx() 而不是 xxx?

答案:Kotlin 属性编译后是 一个私有字段 + getter/setter——遵循 JavaBean 规范。Java 侧只能通过 getter/setter 访问;想让 Java 直接看到字段,就加 @JvmField(此时 Kotlin 不再生成 getter/setter,属性变成公有字段)。


# 3.8 综合案例练习

# 3.8.1 网络请求实战

需求:并发拉取用户信息 + 用户订单,任一失败整体失败;最长超时 3 秒;结果通过扩展函数封装成 Result。

import kotlinx.coroutines.*

// ============ 数据模型 ============
data class User(val id: Long, val name: String)
data class Order(val id: Long, val amount: Long)

sealed class ApiResult<out T> {
    data class Ok<T>(val data: T) : ApiResult<T>()
    data class Err(val err: Throwable) : ApiResult<Nothing>()
}

// ============ 扩展函数:把 suspend 块封装成 ApiResult ============
suspend inline fun <T> runCatchingApi(block: suspend () -> T): ApiResult<T> = try {
    ApiResult.Ok(block())
} catch (e: Throwable) {
    ApiResult.Err(e)
}

// ============ 模拟接口 ============
suspend fun fetchUser(id: Long): User {
    delay(500)
    return User(id, "K")
}
suspend fun fetchOrders(userId: Long): List<Order> {
    delay(800)
    return listOf(Order(101, 8999), Order(102, 199))
}

// ============ 业务函数 ============
suspend fun loadHomePage(userId: Long): ApiResult<Pair<User, List<Order>>> =
    runCatchingApi {
        withTimeout(3_000) {
            coroutineScope {
                val user = async { fetchUser(userId) }
                val orders = async { fetchOrders(userId) }
                user.await() to orders.await()
            }
        }
    }

// ============ 使用 ============
fun main() = runBlocking {
    when (val r = loadHomePage(1L)) {
        is ApiResult.Ok -> println("OK: user=${r.data.first}, orders=${r.data.second}")
        is ApiResult.Err -> println("ERR: ${r.err.message}")
    }
}

输出:

OK: user=User(id=1, name=K), orders=[Order(id=101, amount=8999), Order(id=102, amount=199)]

综合运用:

  • 协程结构化并发(coroutineScope { async; async; await })
  • 超时保护(withTimeout)
  • 扩展函数封装(runCatchingApi)
  • sealed class 建模结果(ApiResult)
  • when 穷尽分支

# 3.8.2 UI状态DSL

Compose 风格的 UI 状态 DSL:

class Screen {
    private val components = mutableListOf<String>()

    fun toolbar(block: Toolbar.() -> Unit) {
        components += Toolbar().apply(block).render()
    }
    fun content(block: Content.() -> Unit) {
        components += Content().apply(block).render()
    }

    fun render() = components.joinToString("\n")

    class Toolbar {
        var title: String = ""
        var back: Boolean = false
        fun render() = "[Toolbar] title=$title back=$back"
    }
    class Content {
        private val items = mutableListOf<String>()
        fun text(msg: String) { items += "  Text: $msg" }
        fun button(label: String, action: () -> Unit = {}) { items += "  Button: $label" }
        fun render() = "[Content]\n" + items.joinToString("\n")
    }
}

fun screen(block: Screen.() -> Unit) = Screen().apply(block).render()

fun main() {
    val ui = screen {
        toolbar {
            title = "首页"
            back = false
        }
        content {
            text("欢迎来到 Kotlin")
            button("点我") { println("clicked") }
            button("退出")
        }
    }
    println(ui)
}

输出:

[Toolbar] title=首页 back=false
[Content]
  Text: 欢迎来到 Kotlin
  Button: 点我
  Button: 退出

# 3.9 训练题三道

训练题 1(DSL 设计):为 SQL 查询设计一个 Kotlin DSL —— select { from("user").where { age gt 18 } }。

参考答案
class SelectBuilder {
    private var from: String = ""
    private var where: String = ""
    fun from(table: String): SelectBuilder { this.from = table; return this }
    fun where(block: WhereBuilder.() -> Unit): SelectBuilder {
        where = WhereBuilder().apply(block).build()
        return this
    }
    fun build() = "SELECT * FROM $from" + if (where.isNotEmpty()) " WHERE $where" else ""
}

class WhereBuilder {
    private val conditions = mutableListOf<String>()
    infix fun String.gt(value: Int) { conditions += "$this > $value" }
    infix fun String.eq(value: Any) { conditions += "$this = '$value'" }
    fun build() = conditions.joinToString(" AND ")
}

fun select(block: SelectBuilder.() -> String) = SelectBuilder().apply { block() }.build()

fun main() {
    val sql = select {
        from("user").where {
            "age" gt 18
            "status" eq "active"
        }.build()
    }
    println(sql)
}

输出:SELECT * FROM user WHERE age > 18 AND status = 'active'

陷阱点:中缀函数必须是 单参数扩展/成员函数,infix 只能修饰恰好一个参数的函数。

训练题 2(扩展函数封装):为 LiveData<T> 写一个 observe { } 扩展,简化 Observer<T> 匿名类写法。

参考答案
// 假设有 LiveData / Observer / LifecycleOwner 接口(简化模拟)
interface Observer<T> { fun onChanged(t: T) }
class LiveData<T> {
    fun observe(owner: LifecycleOwner, obs: Observer<T>) { /* ... */ }
}
interface LifecycleOwner

// 扩展:把 Observer<T> 匿名类改成 Lambda
inline fun <T> LiveData<T>.observe(owner: LifecycleOwner, crossinline block: (T) -> Unit) {
    observe(owner, object : Observer<T> {
        override fun onChanged(t: T) = block(t)
    })
}

// 使用侧
class ActivityMain : LifecycleOwner {
    val vm: LiveData<Int> = LiveData()
    fun onCreate() {
        vm.observe(this) { value -> println("changed: $value") }   // 一行搞定
    }
}

陷阱点:inline 函数里用 Lambda 构造匿名 Observer——Lambda 需要 crossinline,否则内部匿名类不能捕获 Lambda。

训练题 3(协程封装):写一个 withTimeoutOrDefault(timeMs: Long, default: T, block: suspend () -> T),超时返回默认值。

参考答案
import kotlinx.coroutines.*

suspend fun <T> withTimeoutOrDefault(
    timeMs: Long,
    default: T,
    block: suspend () -> T
): T = try {
    withTimeout(timeMs) { block() }
} catch (e: TimeoutCancellationException) {
    default
}

fun main() = runBlocking {
    val fast = withTimeoutOrDefault(1000, "default") {
        delay(200); "quick"
    }
    val slow = withTimeoutOrDefault(300, "default") {
        delay(1000); "slow"
    }
    println("fast=$fast, slow=$slow")
}

输出:fast=quick, slow=default

陷阱点:withTimeout 超时抛的是 TimeoutCancellationException(而非普通 Exception),需要精确捕获——用 catch (e: Exception) 会误吞其他协程异常。


# 3.10 新手陷阱Top5

# 陷阱 症状 正确姿势
1 inline 过度或不当使用 JAR 膨胀 / public inline 破坏 ABI 只在接受 Lambda 的高阶函数上用
2 suspend 不能在裸 Lambda 里用 编译报错 Lambda 声明为 suspend () -> Unit
3 launch 未捕获异常 静默崩溃 用 CoroutineExceptionHandler 或 try-catch
4 扩展函数被当多态 覆盖失败 记住「扩展是静态派发」
5 companion object 未加 @JvmStatic 让 Java 调用蛋疼 Java 侧多一层 .Companion. SDK 场景务必标 @JvmStatic

扩展陷阱 4 项:

  • 陷阱 6:GlobalScope.launch 泄漏——GlobalScope 的协程不受任何生命周期约束,Activity 销毁后仍在跑。永远用 viewModelScope / lifecycleScope / coroutineScope。
  • 陷阱 7:runBlocking 里的 Dispatchers.Main 死锁——在主线程调 runBlocking { withContext(Main) { } },主线程被 runBlocking 阻塞、withContext(Main) 又等主线程,永远等不到。
  • 陷阱 8:apply 里返回值被吞——val x = Config().apply { debug = true; loadCache() }——loadCache() 的返回值被 apply 吞了,只返回 Config。想拿到值应用 run 或 let。
  • 陷阱 9:?.let { } 的 return 语义——return 在 Lambda 里默认是 非局部返回(返回 外层函数),不是「返回 Lambda」。想只返回 Lambda 用 return@let。

下一章预告:入门篇到此结束。接下来进入 核心原理专栏,我们将沉到字节码级别,揭开 Kotlin 每一个语法糖背后的真相。

上次更新: 2026/07/05, 15:10:54
类和对象
空安全类型原理

← 类和对象 空安全类型原理→

最近更新
01
11.数据库的原理总结
07-12
02
11.操作系统原理总结
07-10
03
资深程序员软能力
07-10
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式