区间与数列:IntRange、LongRange、CharRange、progression、自定义区间
区间这个东西,说白了就是「从哪到哪」的一种表达。在 Kotlin 里,它不是一个简单的语法糖,而是一套完整的类型体系。我个人习惯把区间理解成「带边界的连续值集合」,而数列(Progression)则是「带步长的区间」。嗯,咱们今天就把这块彻底聊透。
一、区间的基本形态
Kotlin 提供了三种最常用的区间类型:IntRange、LongRange 和 CharRange。它们都实现了 ClosedRange<T> 接口,核心就是两个属性:start 和 endInclusive。
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 就派上用场了——我可以生成一个带步长的数列,只展示关键页码。
小技巧:downTo 和 step 的顺序不能乱。必须是 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>。自定义区间如果想支持遍历,需要自己实现迭代逻辑。
五、知识体系总览
下面这张图总结了区间与数列的核心关系,我建议你保存下来,写代码时对照着看:
六、实战中的选择建议
| 场景 | 推荐类型 | 原因 |
|---|---|---|
| 判断值是否在范围内 | IntRange / LongRange |
直接用 in 操作符,性能最优 |
| 遍历连续整数 | IntRange |
支持 for 循环,代码简洁 |
| 遍历带步长的整数 | IntProgression |
使用 step 或 downTo |
| 字符范围判断 | Char.isLetter() 等 |
避免 Unicode 码点陷阱 |
| 自定义对象范围 | 实现 Comparable + ClosedRange |
灵活但需手动处理迭代 |
我个人在实际项目中,80% 的场景只用 IntRange 做范围判断,15% 用 IntProgression 做步进遍历,剩下 5% 才是自定义区间。你想想看,其实大部分业务逻辑都是「判断某个数在不在区间里」,而不是「遍历区间里的每个数」。所以别把区间想得太复杂,它本质上就是个带边界检查的容器。
性能提示:in 操作符对于 IntRange 和 LongRange 是 O(1) 的,因为它只比较 start 和 endInclusive。但对于自定义区间,in 会调用 compareTo 方法,性能取决于你的实现。
好了,区间和数列这块就聊到这儿。记住一个核心原则:能用标准区间就用标准区间,别自己造轮子。Kotlin 的标准库已经覆盖了 90% 的场景,剩下的 10% 再考虑自定义。
公众号:蓝海资料掘金营,微信deep3321