区间、跳转、解构与 this 表达式

本章整理几个在 Kotlin 代码里出现频率很高、但经常被 Java 开发者低估的语言机制:

  • 区间与数列进程:1..101..<10downTostep
  • 返回与跳转:returnbreakcontinue、标签返回、非局部返回。
  • 解构声明:val (name, age) = person
  • this 表达式:成员、扩展函数、带接收者 lambda、多层作用域中的 this@label

这些特性看起来分散,但背后有共同点:Kotlin 喜欢用“约定 + 表达式 + 作用域”减少样板代码。读懂这些特性,才能真正读懂 Kotlin 标准库、集合操作、DSL、协程和框架代码。

区间与数列进程

Kotlin 用区间表达一段连续值:

fun main() {
    println(3 in 1..5)   // true
    println(5 in 1..<5)  // false
}

常见写法:

val closed = 1..4      // 1, 2, 3, 4
val openEnd = 1..<4    // 1, 2, 3
val reversed = 4 downTo 1
val even = 0..8 step 2

官方文档把它分成两层:

  • range:有开始和结束的有序值集合。
  • progression:可以迭代的等差进程,包含 firstlast、非 0 的 step

Java 对比:

for (int i = 1; i <= 4; i++) {
    System.out.print(i);
}

Kotlin:

for (i in 1..4) {
    print(i)
}

这不是简单语法糖。1..4 会调用 rangeTo 约定函数,数值区间会形成对应的 progression,可用于 for 循环和集合函数。

闭区间与开区间

.. 是闭区间,包含右边界:

for (i in 0..3) {
    print(i) // 0123
}

..< 是开区间,不包含右边界:

for (i in 0..<3) {
    print(i) // 012
}

处理索引时,开区间通常更自然:

val names = listOf("Ada", "Bob", "Cora")

for (index in 0..<names.size) {
    println("$index -> ${names[index]}")
}

不过 Kotlin 集合已经提供更好的索引遍历:

for ((index, name) in names.withIndex()) {
    println("$index -> $name")
}

Java 对比:Java 常用 i < list.size(),所以 Kotlin 的 0..<size 更接近 Java 索引循环;0..size 则会越界。

downTo 与 step

倒序用 downTo

for (i in 5 downTo 1) {
    print(i) // 54321
}

步长用 step

for (i in 0..8 step 2) {
    print(i) // 02468
}

倒序也可以结合步长:

for (i in 8 downTo 0 step 2) {
    print(i) // 86420
}

注意 progression 的最后一个元素不一定等于你写的右边界:

for (i in 1..9 step 3) {
    print(i) // 147
}

因为从 1 开始每次加 3,下一项是 10,已经超过 9,所以最后一项是 7。

区间不是 List

不要把 range 当作已经分配好的 List

val range = 1..1_000_000

这不是创建一百万个整数放进列表。它表示一个范围对象,可以按需要迭代。

如果你真的要列表:

val numbers: List<Int> = (1..10).toList()

Java 对比:Java 传统 for 循环没有“区间对象”这一层;Java Stream 的 IntStream.range(0, n) 更接近 Kotlin 的 range/progression 思路。

返回与跳转

Kotlin 有三种结构化跳转表达式:

  • return:默认从最近的具名函数或匿名函数返回。
  • break:终止最近的循环。
  • continue:进入最近循环的下一轮。

官方文档强调,这些跳转表达式的类型是 Nothing,所以可以放进更大的表达式里:

fun printName(person: Person?) {
    val name = person?.name ?: return
    println(name)
}

return 在这里是 Elvis 表达式右侧。它不会产生值,而是直接退出函数。因为 return 的类型是 Nothing,可以兼容左侧需要的任何类型。

Java 对比:

void printName(Person person) {
    if (person == null || person.getName() == null) {
        return;
    }
    String name = person.getName();
    System.out.println(name);
}

Kotlin 更倾向用提前返回表达“没有值就退出”。

break 与 continue 标签

普通 break 只跳出最近的循环:

for (i in 1..3) {
    for (j in 1..3) {
        if (i == 2 && j == 2) break
        println("$i,$j")
    }
}

如果要跳出外层循环,可以使用标签:

outer@ for (i in 1..3) {
    for (j in 1..3) {
        if (i == 2 && j == 2) break@outer
        println("$i,$j")
    }
}

continue@outer 则进入外层循环的下一轮:

outer@ for (row in rows) {
    for (cell in row.cells) {
        if (cell.invalid) continue@outer
    }
    process(row)
}

Java 也有标签 break label;continue label;,但在 Java 项目里并不常见。Kotlin 中标签更常出现在 lambda 返回里,必须掌握。

Lambda 中的 return

这是 Java 开发者最容易误判的地方。

先看普通高阶函数:

fun runBlock(block: () -> Unit) {
    block()
}

fun demo() {
    runBlock {
        // return // 编译错误:不能从这里直接 return demo()
    }
}

lambda 不是具名函数,普通情况下不能用裸 return 退出外层函数。你可以使用标签返回:

fun demo() {
    runBlock label@{
        println("before")
        return@label
    }
    println("after")
}

如果 lambda 传给的函数名是 runBlock,也可以用隐式标签:

fun demo() {
    runBlock {
        return@runBlock
    }
}

集合操作中很常见:

fun printNonBlank(values: List<String>) {
    values.forEach {
        if (it.isBlank()) return@forEach
        println(it)
    }
}

这里 return@forEach 类似传统循环里的 continue,只跳过当前 lambda 处理。

非局部返回

如果 lambda 传给的是 inline 函数,Kotlin 允许某些非局部返回:

fun containsZero(values: List<Int>): Boolean {
    values.forEach {
        if (it == 0) return true
    }
    return false
}

这里 return true 返回的是 containsZero,不是只返回 forEach 的 lambda。原因是标准库 forEach 是 inline 函数,lambda 代码会被内联到调用点。

Java 对比:Java lambda 中的 return 只能从 lambda 自身返回,不能从外层方法返回:

values.forEach(value -> {
    if (value == 0) {
        return; // 只结束当前 lambda 调用
    }
});

Kotlin 的非局部返回很强,但也会让代码可读性变差。复杂逻辑里,传统 for 循环往往更清晰:

fun containsZero(values: List<Int>): Boolean {
    for (value in values) {
        if (value == 0) return true
    }
    return false
}

用 run 模拟 lambda 中的 break

lambda 中没有直接等价于 break 的语法。可以用带标签的 run 包住:

fun printUntilZero(values: List<Int>) {
    run loop@{
        values.forEach {
            if (it == 0) return@loop
            println(it)
        }
    }
}

但这类写法不适合过度使用。多数情况下,普通 for 循环更清楚:

fun printUntilZero(values: List<Int>) {
    for (value in values) {
        if (value == 0) break
        println(value)
    }
}

不要为了“函数式风格”牺牲控制流可读性。

匿名函数中的 return

匿名函数和 lambda 不同:

fun demo(values: List<Int>) {
    values.forEach(fun(value: Int) {
        if (value == 0) return
        println(value)
    })

    println("done")
}

这里 return 返回的是匿名函数本身,类似 return@forEach。如果你想避免标签,可以用匿名函数表达“局部返回”。

解构声明

解构声明一次创建多个变量:

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

fun main() {
    val user = User("Ada", 36)
    val (name, age) = user

    println(name)
    println(age)
}

它会被编译成类似:

val name = user.component1()
val age = user.component2()

所以解构依赖 componentN() 函数。数据类会自动生成 componentN(),普通类也可以自己定义,但必须使用 operator

class Point(
    private val x: Int,
    private val y: Int,
) {
    operator fun component1(): Int = x
    operator fun component2(): Int = y
}

val (x, y) = Point(10, 20)

Java 对比:Java 没有通用解构声明。Java record 支持组件访问器,但不能像 Kotlin 一样直接 val (x, y) = point

Map 遍历中的解构

解构最常见的场景是遍历 Map:

val scores = mapOf("Ada" to 98, "Bob" to 85)

for ((name, score) in scores) {
    println("$name -> $score")
}

标准库为 Map.Entry 提供了 component1()component2(),所以能这样写。

Java 对比:

for (Map.Entry<String, Integer> entry : scores.entrySet()) {
    String name = entry.getKey();
    Integer score = entry.getValue();
}

Kotlin 解构减少了样板,但要记住它是按组件位置取值,不是魔法。

下划线忽略组件

不需要的组件可以用 _

val (_, status) = getResult()

_ 忽略的组件不会调用对应的 componentN()。这点在组件计算有成本时有意义。

lambda 参数里也可以用:

scores.mapValues { (_, score) -> score + 1 }

Lambda 参数解构

注意这三种写法不同:

{ entry -> entry.value }       // 一个参数
{ key, value -> value }        // 两个参数
{ (key, value) -> value }      // 一个参数,但被解构成两个变量

map.mapValues { (key, value) -> ... } 中 lambda 实际接收的是一个 Map.Entry<K, V>,然后解构出 key 和 value。

如果需要写类型,可以给整体写:

scores.mapValues { (_, score): Map.Entry<String, Int> ->
    score + 1
}

也可以给单个组件写:

scores.mapValues { (_, score: Int) ->
    score + 1
}

Pair、Triple 与具名数据类

可以用 Pair 返回两个值:

fun parseName(input: String): Pair<String, String> {
    val parts = input.split(" ", limit = 2)
    return parts[0] to parts.getOrElse(1) { "" }
}

val (firstName, lastName) = parseName("Ada Lovelace")

但公开 API 中,具名数据类通常更清楚:

data class ParsedName(
    val firstName: String,
    val lastName: String,
)

fun parseName(input: String): ParsedName {
    val parts = input.split(" ", limit = 2)
    return ParsedName(
        firstName = parts[0],
        lastName = parts.getOrElse(1) { "" },
    )
}

Pair<String, String> 很难表达两个字符串分别是什么。Java 开发者也经常遇到类似问题:Map.EntryPair、数组返回值都容易让语义丢失。

name-based destructuring

官方解构文档已经加入 name-based destructuring。它是实验性特性,允许按属性名而不是 componentN() 位置匹配变量。

传统位置解构:

data class User(val username: String, val email: String)

val user = User("alice", "alice@example.com")
val (email, username) = user

println(email)    // alice
println(username) // alice@example.com

这里变量名不会影响取值顺序,email 拿到的是第一个组件 username

name-based destructuring 的目标是减少这种错位风险。例如显式形式:

// 实验性语法,需编译器选项支持
(val mail = email, val name = username) = user

官方文档还提到,通过 -Xname-based-destructuring 可以控制编译器模式,例如:

  • only-syntax:启用显式 name-based destructuring 语法,不改变现有解构行为。
  • name-mismatch:在数据类位置解构变量名与属性名不匹配时报警告。
  • complete:启用短形式的按名解构,并继续用方括号支持位置解构。

因为该能力仍是实验性的,生产项目不建议盲目开启 complete。更稳妥的做法是:

  • 公开 API 中优先使用具名类型和清晰属性。
  • 解构时变量名和属性顺序保持一致。
  • PairTriple 这类位置语义强的类型,解构变量名要格外清楚。

this 表达式

this 表示当前接收者:

  • 在类成员里,this 是当前对象。
  • 在扩展函数里,this 是扩展接收者。
  • 在带接收者的 lambda 中,this 是 lambda 的接收者。

类成员中的 this

class Counter {
    private var value = 0

    fun increment() {
        this.value += 1
    }
}

this. 可以省略:

fun increment() {
    value += 1
}

扩展函数中的 this

fun String.firstOrDash(): Char =
    if (this.isEmpty()) '-' else this[0]

带接收者 lambda:

val text = buildString {
    append("Hello")
    append(", ")
    append("Kotlin")
}

buildString {} 的 lambda 接收者是 StringBuilder,所以可以直接调用 append()

Java 对比:Java lambda 没有 Kotlin 这种“带接收者 lambda”。Java 中 this 通常仍指向外层对象,而不是 lambda 的某个接收者。

this@label

当多个接收者嵌套时,用 this@label 指明你要哪个 this

class HtmlPage {
    fun render() {
        buildString page@{
            append("<html>")

            listOf("Home", "Docs").forEach { title ->
                this@page.append("<a>$title</a>")
            }

            append("</html>")
        }
    }
}

在内部类和扩展函数混合时更明显:

class Outer {
    private val name = "outer"

    inner class Inner {
        fun String.describe(): String {
            val outer = this@Outer.name
            val inner = this@Inner
            val receiver = this
            return "$outer / $inner / $receiver"
        }
    }
}

官方文档示例也展示了:

  • this@A 指外层类。
  • this@B 指内部类。
  • this@foo 指扩展函数接收者。
  • lambda 可以用显式标签区分接收者。

隐式 this 的陷阱

如果成员函数和局部函数同名,省略 this. 可能调用到你没预期的函数:

fun main() {
    fun printLine() {
        println("Local function")
    }

    class Printer {
        fun printLine() {
            println("Member function")
        }

        fun invoke(omitThis: Boolean) {
            if (omitThis) {
                printLine()
            } else {
                this.printLine()
            }
        }
    }

    Printer().invoke(omitThis = true)  // Local function
    Printer().invoke(omitThis = false) // Member function
}

在 DSL 或嵌套 lambda 中,如果出现多个可用接收者,建议显式写 this@label 或拆小函数,避免靠读者猜作用域。

实践建议

  • 索引范围优先用 0..<size,避免把右边界包含进去。
  • 遍历集合优先用集合 API,例如 indiceswithIndex(),不要手写复杂区间。
  • lambda 中遇到复杂 return@label 时,考虑改成普通 for 循环。
  • 解构变量名要和组件语义一致,避免 val (email, username) = user 这种错位。
  • 公开 API 少用裸 Pair/Triple,优先具名数据类。
  • 多层接收者里不要过度依赖隐式 this,必要时使用 this@label
  • 读 Kotlin DSL 时,先判断当前 lambda 有没有 receiver,再判断未限定函数到底调用的是谁。

官方参考

  • Ranges and progressions:https://kotlinlang.org/docs/ranges.html
  • Returns and jumps:https://kotlinlang.org/docs/returns.html
  • Destructuring declarations:https://kotlinlang.org/docs/destructuring-declarations.html
  • This expressions:https://kotlinlang.org/docs/this-expressions.html