区间与数列:IntRange、LongRange、CharRange、progression、自定义区间

区间这个东西,说白了就是「从哪到哪」的一种表达。在 Kotlin 里,它不是一个简单的语法糖,而是一套完整的类型体系。我个人习惯把区间理解成「带边界的连续值集合」,而数列(Progression)则是「带步长的区间」。嗯,咱们今天就把这块彻底聊透。

一、区间的基本形态

Kotlin 提供了三种最常用的区间类型:IntRangeLongRangeCharRange。它们都实现了 ClosedRange<T> 接口,核心就是两个属性:startendInclusive

val intRange: IntRange = 1..10
val longRange: LongRange = 100L..200L
val charRange: CharRange = 'a'..'z'

println(intRange.start)      // 1
println(intRange.endInclusive) // 10
println('m' in charRange)    // true

你想想看,1..10 这种写法多直观。我在项目中经常用它来做范围校验,比如用户输入的年龄必须在 0..150 之间。但要注意,.. 操作符默认是包含终点的,这和很多语言的左闭右开不一样。

核心区别1..10 包含 10,而 1 until 10 不包含 10。后者返回的是 IntRange 吗?其实不是,它返回的是 IntProgression

二、数列(Progression)—— 带步长的区间

区间只能表示连续的整数或字符,但实际开发中我们经常需要「跳着走」。比如每隔 2 天执行一次任务,或者从 100 往下数到 0。这时候就要用到 Progression

val stepUp: IntProgression = 1..10 step 2   // 1, 3, 5, 7, 9
val stepDown: IntProgression = 10 downTo 1 step 3 // 10, 7, 4, 1

for (i in stepUp) {
    print("$i ")  // 输出: 1 3 5 7 9
}

我记得有一次做分页功能,后端返回了总条数,我需要生成一个「页码列表」展示在 UI 上。如果用 1..totalPages 直接遍历,当总页数很大时性能没问题,但 UI 上要显示省略号。这时候 step 就派上用场了——我可以生成一个带步长的数列,只展示关键页码。

小技巧downTostep 的顺序不能乱。必须是 10 downTo 1 step 3,不能写成 10 step 3 downTo 1。这是 Kotlin 中缀函数调用顺序决定的。

三、CharRange 的特殊之处

CharRange 看起来和 IntRange 很像,但底层实现有区别。字符区间本质上是对 Unicode 码点的遍历,所以 'a'..'z' 是连续的,但 'A'..'z' 中间会包含一些标点符号(比如 [\] 等)。

val mixedRange = 'A'..'z'
mixedRange.forEach { print(it) }
// 输出: ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz

我曾经在做一个密码强度检测器时踩过这个坑。我想用 'a'..'z' 判断字符是否是小写字母,结果发现 'a'..'z' 没问题,但 'A'..'z' 会把 [ 也算进去。嗯,后来我改用 Char.isLetter()Char.isLowerCase() 组合判断了。

避坑指南:不要用 CharRange 做字母分类判断。Unicode 码点不是按字母表顺序完美排列的。用标准库的 Char.isLetter()Char.isDigit() 等方法更安全。

四、自定义区间

Kotlin 的区间系统是可扩展的。只要你的类型实现了 Comparable 接口,就可以创建自定义区间。比如我们定义一个 Version 类,表示软件版本号:

data class Version(val major: Int, val minor: Int, val patch: Int) : Comparable<Version> {
    override fun compareTo(other: Version): Int {
        return compareValuesBy(this, other, { it.major }, { it.minor }, { it.patch })
    }
}

fun main() {
    val v1 = Version(1, 0, 0)
    val v2 = Version(2, 5, 3)
    val range = v1..v2

    println(Version(1, 9, 9) in range)  // true
    println(Version(3, 0, 0) in range)  // false
}

但注意,自定义区间默认只能做 in 检查,不能直接 for 遍历。因为 for 循环需要 Iterable 接口,而 ClosedRange 本身不提供迭代能力。要实现可迭代的自定义区间,需要同时实现 Iterable 或继承 IntProgression 的类似机制。

深入理解IntRange 之所以能 for 遍历,是因为它实现了 IntProgression,而 IntProgression 实现了 Iterable<Int>。自定义区间如果想支持遍历,需要自己实现迭代逻辑。

五、知识体系总览

下面这张图总结了区间与数列的核心关系,我建议你保存下来,写代码时对照着看:

ClosedRange<T> start / endInclusive IntRange LongRange CharRange IntProgression first / last / step 自定义区间 (Comparable) 实线:继承关系 | 虚线:实现关系 | 自定义区间需实现 Comparable

六、实战中的选择建议

场景 推荐类型 原因
判断值是否在范围内 IntRange / LongRange 直接用 in 操作符,性能最优
遍历连续整数 IntRange 支持 for 循环,代码简洁
遍历带步长的整数 IntProgression 使用 stepdownTo
字符范围判断 Char.isLetter() 避免 Unicode 码点陷阱
自定义对象范围 实现 Comparable + ClosedRange 灵活但需手动处理迭代

我个人在实际项目中,80% 的场景只用 IntRange 做范围判断,15% 用 IntProgression 做步进遍历,剩下 5% 才是自定义区间。你想想看,其实大部分业务逻辑都是「判断某个数在不在区间里」,而不是「遍历区间里的每个数」。所以别把区间想得太复杂,它本质上就是个带边界检查的容器。

性能提示in 操作符对于 IntRangeLongRange 是 O(1) 的,因为它只比较 startendInclusive。但对于自定义区间,in 会调用 compareTo 方法,性能取决于你的实现。

好了,区间和数列这块就聊到这儿。记住一个核心原则:能用标准区间就用标准区间,别自己造轮子。Kotlin 的标准库已经覆盖了 90% 的场景,剩下的 10% 再考虑自定义。


公众号:蓝海资料掘金营,微信deep3321