本文收录于 kotlin入门潜修专题系列,欢迎学习交流。
创作不易,如有转载,还请备注。
写在前面
人的一生,应当像这美丽的花,自己无所求,而却给人间以美。——与君共勉。
本篇文章内容
本篇文章将会阐述kotlin中的两个特性:解构和Ranges,并分析他们背后的实现原理。
解构声明
kotlin为我们提供了很方便的解构功能,什么是解构?看个例子就明白了:
//注意,此处声明了一个data数据类
data class Person(val name: String, val age: Int) {}
//测试main方法
fun main(args: Array<String>) {
//这种写法就是解构
val (name, age) = Person("zhangsan", 20)
println("name: $name, age: $age")
}
上面的写法就是解构,由代码可知,解构可以大大简化代码。但是上面还有一个需要注意,实际上我们声明的Person类是个data类,换做普通类行不行?
答案是不行的。这就涉及到解构的运行原理了。
解构的使用必须要求类提供componentN方法,而且该系列方法需要使用operator关键字修饰。因为解构在编译的时候实际上是通过componentN方法来完成值获取的,比如上面代码 val (name, age) = Person("zhangsan", 20)对应的字节码如下所示(截取部分字节码):
LDC "zhangsan"
BIPUSH 20
INVOKESPECIAL Person.<init> (Ljava/lang/String;I)V
ASTORE 3
ALOAD 3
INVOKEVIRTUAL Person.component1 ()Ljava/lang/String;
ASTORE 1
ALOAD 3
INVOKEVIRTUAL Person.component2 ()I
很显然,kotlin是通过 Person.component1 ()来获取name值,通过 Person.component2来获取age值。那么为什么data类会有componentN方法呢?这个答案已经在文章kotlin入门潜修之类和对象篇—数据类及其原理中详细阐述过了,总之,一句话这个就是data数据类的特性。
那么有没有方法在普通类中使用解构?答案是肯定的,前面我们已经知道了解构背后的原理,所以我们只需要在普通类中定义对应的component系列方法即可。需要说明的是,component方法实际上与属性成员一一对应,所以有多少个属性,理当提供多少个component方法。示例如下:
//我们自定义普通类,使之支持解构
class Person(val name: String, val age: Int) {
//operator关键字是必须的,方法的名字的命名规范是component+数字,
//数字从1开始依次递增。
operator fun component1(): String {
return this.name
}
operator fun component2(): Int {
return age
}
}
//测试方法
fun main(args: Array<String>) {
//使用方式同数据类一致
val (name, age) = Person("zhangsan", 20)
println("name: $name, age: $age")
}
解构的另一个用处就是方便方法进行多值返回。什么是多值返回?就是一次性返回多个值。一次性返回多个值直接使用对象不就行了?是可以的,但是如果我们想获取指定几个值的话,使用对象就显得比较麻烦,至少还要通过获取到的对象进行属性访问。而解构可以很简单,如下所示:
//定义了一个方法getPersonAge,用于获取person的年龄
fun getPersonAge(): Person {
return Person("zhangsan", 20)
}
//测试方法
fun main(args: Array<String>) {
val (_, age) = getPersonAge()//这里使用解构来获取age
println("age: $age")
}
上面代码表明了使用解构的方便之处。有一点需要注意的是,解构声明实际上会按照属性的顺序依次调用componentN方法,所以当我们不需要某个属性的时候,可以使用_来代替,上面代码就表示我们不需要name信息。
在kotlin的标准库中,提供了很多可以使用解构的方法,比如我们常用的map,如下所示:
//生成一个map对象
val map = mapOf(
"name" to "zhangsan",
"age" to 20)
//遍历map中的key 和 value
for ((key, value) in map) {
println("key: $key, value:$value")
}
上面代码执行完后,打印如下:
key: name, value:zhangsan
key: age, value:20
最后再来探索下,为什么map能够在for...in循环中使用解构?实际上要在for... in循环中使用解构,需要满足以下条件:
- 必须提供有iterator。这个条件是使用for...in循环的必要条件,实际上map确实是满足的,map定义了扩展方法iterator:public inline operator fun <K, V> Map<out K, V>.iterator(): Iterator<Map.Entry<K, V>> = entries.iterator()
- 必须要有compnentN方法。这个是使用解构的必要条件,实际上map也是满足的,因为map为key和value也定义了对应的component扩展方法,如下所示:
public inline operator fun <K, V> Map.Entry<K, V>.component1(): K = key
public inline operator fun <K, V> Map.Entry<K, V>.component2(): V = value
对于map的结构,实际上从mapOf的入参类型也可以看出来,因为mapOf实际上接收的是Pair<K, V>类型,而这个正式kotlin提供的默认data类,具体可见kotlin入门潜修之类和对象篇—数据类及其原理这篇文章。
kotlin同时支持在lambda表达式中使用解构,如下所示:
val map = mapOf(
"name" to "zhangsan",
"age" to 20)
//解构用于lambda中
map.mapValues { (_, value) -> println("$value") }//打印zhangsan 20
解构用于lambda中时,需要注意的一点是和lamba入参的区别,使用()括起来的时候表示使用解构,否则表示是lambda入参,如下所示:
//!!!下面代码不可执行,就是来说明下lambda入参和解构的区别
{ a -> ... }//没有括号,表示一个入参a
{ a, b -> ... }//没有括号,表示两个入参a、b
{ (a, b) -> ... }//有括号,表示解构
Range表达式
来看下什么Range表达式,示例如下:
fun main(args: Array<String>) {
val i = 3
if (i in 1..10) {//这个就是Range表达式
println(i)
}
}
上面1..10就表示一个Range表达式,那么这个..到底是什么?在idea ide中,我们可以将鼠标放在..上面,然后通过右键->Go To ->Declaration跳到其定义处(或者按着command按键+鼠标左键跳转),很惊奇的发现,竟然指向了下面一个操作符方法:
/** Creates a range from this value to the specified [other] value. */
public operator fun rangeTo(other: Int): IntRange
该代码位于Primitives.kt文件中,从代码的注释可以看出,这个方法主要是创建了一个指定开始和结束的整型数字范围。实际上通过查看Primitives.kt发现,该文件中还有很多这种操作符,主要对应于不同的类型。
既然..实际上就是rangeTo操作符,那么是不是也可以直接通过rangeTo操作符来完成上面操作?答案是肯定的,但是因为rangeTo并不是中缀方法,所以只能通过方法调用的方式实现。如下所示:
if (i in 1.rangeTo(10)) {
println(i)
}
range表达式也可以用于for循环中,如下所示:
fun main(args: Array<String>) {
for (i in 1..10) {
println(i)//打印1-10 数字
}
}
上面是正序打印,如果想逆序打印,则可以使用kotlin为我们提供的downTo操作符,如下所示:
fun main(args: Array<String>) {
for (i in 10 downTo 1) {
println(i)
}
}
上面代码中,downTo实际上连接的是10 和 1,in操作符则作用的10 downTo 1的结果,由此也可推知downTo应该是个中缀方法,查看其定义确实如此,如下所示:
public infix fun Int.downTo(to: Int): IntProgression {
return IntProgression.fromClosedRange(this, to, -1)
}
downTo最后返回了IntProgression类,这个类提供了iterator,所以可以用于for..in当中。
另外,如果我们想输出间隔特定步长的值,则可以提供kotlin为我们提供的另一个操作符:step,如下所示:
fun main(args: Array<String>) {
for (i in 10 downTo 1 step 2) {
println(i)//打印1到10之间的偶数:2 4 6 8 10
}
}
step也是个中缀方法,在这里返回和downTo一致,都是IntProgression,所以也可以用在for..in中。还有个问题,多个中缀方法显然是左结合的,所以上面代码可以表示为(10 downTo 1)step 2,但是因为中缀方法要求receiver和入参相同,10 downTo 1的返回值是IntProgression,那么step的receiver理论上也必须是IntProgression,事实上确实是这样的,通过查看step的定义就会明白:
//很显然,step是IntProgression的扩展方法,其receiver就是IntProgression
public infix fun IntProgression.step(step: Int): IntProgression {
checkStepIsPositive(step > 0, step)
return IntProgression.fromClosedRange(first, last, if (this.step > 0) step else -step)
}
kotlin中还有个关键字until,这个关键字的意思是生成的范围不包括until后面的数字,如下所示:
fun main(args: Array<String>) {
for (i in 1 until 3) {//这里打印1 2
println(i)
}
}
util也是个中缀方法,其源代码如下所示:
public infix fun Int.until(to: Int): IntRange {
if (to <= Int.MIN_VALUE) return IntRange.EMPTY
return this .. (to - 1).toInt()//注意这里
}
很显然,util的实现也是借助于..操作符实现的,只不过其最大值被限制在了to - 1上,所以才不包括to。
最后,需要说明的是,kotlin中并不是Int才对应有range,其他类型也同样有,比如Long、Short、Byte等都有。
range表达式的工作机制
通过对比不同类型的range表达式可以发现,这些range表达式都实现了ClosedRange<T: Comparable<T>>接口,其定义的成员如下所示:
public interface ClosedRange<T: Comparable<T>> {
public val start: T
public val endInclusive: T
public operator fun contains(value: T): Boolean = value >= start && value <= endInclusive
public fun isEmpty(): Boolean = start > endInclusive
}
结合ClosedRange的定义,我们可以总结Ranges(范围)的一些特点:
- Ranges的成员必须是可比较的,即要实现Comparable接口
- Ranges都包含了start 和 endInclusive两个属性,是Range的上下界。
- Ranges都有个contains方法,该方法用于判断Range中是否包含有特定元素,默认是判断是否在start(包括)和endInclusive(baok)之间。
- Ranges同时提供了isEmpty判断,来判断范围内是否有数据,同样是根据2中两个属性进行判断。