kotlin入门潜修之特性及其原理篇—解构和Ranges

本文收录于 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循环中使用解构,需要满足以下条件:

  1. 必须提供有iterator。这个条件是使用for...in循环的必要条件,实际上map确实是满足的,map定义了扩展方法iterator:public inline operator fun <K, V> Map<out K, V>.iterator(): Iterator<Map.Entry<K, V>> = entries.iterator()
  2. 必须要有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(范围)的一些特点:

  1. Ranges的成员必须是可比较的,即要实现Comparable接口
  2. Ranges都包含了start 和 endInclusive两个属性,是Range的上下界。
  3. Ranges都有个contains方法,该方法用于判断Range中是否包含有特定元素,默认是判断是否在start(包括)和endInclusive(baok)之间。
  4. Ranges同时提供了isEmpty判断,来判断范围内是否有数据,同样是根据2中两个属性进行判断。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值