美文网首页
kotlin入门潜修之特性及其原理篇—解构和Ranges

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

作者: 寒潇2018 | 来源:发表于2019-01-11 21:46 被阅读0次

    本文收录于 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中两个属性进行判断。

    相关文章

      网友评论

          本文标题:kotlin入门潜修之特性及其原理篇—解构和Ranges

          本文链接:https://www.haomeiwen.com/subject/jvgtdqtx.html