美文网首页
Kotlin学习(7)重载操作符和其他约定

Kotlin学习(7)重载操作符和其他约定

作者: m1Ku | 来源:发表于2019-07-15 17:38 被阅读0次

    Java标准库中有和类相绑定的语言特性,例如实现Iterable接口的类可以使用在for循环中。Kotlin中也有一些类似的特性,与Java不同的是,不是和特定的类绑定的,Kotlin中是与特定名字的函数绑定的。例如我们在类中定义了一个方法叫plus,我们就可以在这个类的实例上使用+运算符,这就叫做约定

    1.重载算数运算符

    ​ Kotlin中最简单明了的使用约定的例子就是算数运算符。在Java中算数运算符只可以使用在基本数据类型上,+号可以使用在String上。当我们想在BigInteger类上使用+,或者想使用+=添加元素到一个集合中时Java就做不到了。但是Kotlin中就可以做到。

    1.重载二元算数运算符

    ​ 首先从+开始,实现将两个点的坐标值加起来,使用operator修饰符定义一个操作符函数

    data class Point(val x: Int, val y: Int) {
        operator fun plus(other: Point): Point {
            return Point(x + other.x, y + other.y)
        }
    }
    

    ​ 定义Point对象,此时可以使用+号

    val point = Point(10, 20)
    val point2 = Point(20, 30)
    println(point + point2)
    >>Point(x=30, y=50)
    

    ​ 也可以把操作符函数定义成扩展函数,而使用扩展函数语法也会是一种通用的定义操作符扩展函数的模板

    operator fun Point.plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }
    

    ​ Kotlin不允许你自己定义操作符,下面是可以重载的操作符及函数名。为自己写的Kotlin类定义的算数运算符其优先级和数字运算符是相同的。

    表达式 函数名
    a * b times
    a / b div
    a % b mod
    a + b plus
    a - b minus

    ​ 当我们定义操作符时,他接受的两个操作符可以是不同类型的,操作符函数的返回值也可以是不同类型的。但是需要注意的是Kotlin操作符不支持左右两个操作数交换顺序

    operator fun Point.times(scale: Double): Double {
        return x * scale
    }
    

    2.重载复合赋值操作符

    ​ 正常情况下,当定义了一个操作符比如plus时,Kotlin会同时支持+和+=操作符。+=-=等等叫做复合赋值操作符

    //复合赋值运算符
    var point3 = Point(1, 2)
    point3 += Point(2, 4)
    println(point3)
    >> Point(x=3, y=6)
    

    ​ 如果你定义一个叫空返回值的plusAssign的函数,当时用+=操作符时就会调用这个函数。minusAssigntimesAssign也是类似的。Kotlin标准库为为可变集合定义了plusAssign函数。

    ​ 当你在代码中使用+=时,理论上plus和plusAssign都会被调用。我们应该避免同时为添加plus和plusAssign操作符。如果你的类时不可变得,你应该只提供像plus一样返回一个新值的操作符,如果设计一个可变的类,你应该只需要提供一个plusAssign以及类似的操作符。集合操作中,+-会返回一个新的集合;+=-=用在可变集合时会改变他们的值,使用在只读集合时,会返回一个修改了的拷贝集合。(这意味着只有当可读集合的引用是var才可以使用+=和-=)

    3.重载一元操作符

    ​ 一元运算符定义的方法和前面看到的是相同的,重载一元操作符的函数不需要任何参数

    下表是所有可以重载的一元运算符

    表达式 函数名
    + a unaryPlus
    - a unaryMinus
    ! a not
    ++ a,a ++ inc
    -- a,a -- dec

    2.重载比较运算符

    ​ 正如算数运算符一样,Kotlin中允许你将比较运算符(==,!=,>,<等等)用在任何对象上,而不仅仅是基本数据类型

    1.相等运算符:equals

    ==操作符在Kotlin中会转换为equals()函数的调用,!=也是对equals()函数的调用,只是结果相反。另外,相等性操作符的操作数是可空的,因为要比较null和相等性。a == b会先比较a是否为null,再调用a.equals(b)

    equals函数被标记为override,不像其他操作符的约定,不需要加operator标识符,因为他是实现在Any类中的,相等性比价对于任何Kotlin类都是适用的

    2.排序运算符:compareTo

    ​ 在Java类中,类进行查找最大值或者排序时,需要实现Comparable接口。而且进行比较时,没有简短的语法需要显式的调用element1.compareTo(element2)进行比较。

    ​ Kotlin也支持Comparable接口,但是接口中的compareTo方法可以通过约定调用:使用<,>,<=,和>=时会转化为调用compareTo方法。compareTo的返回值为Int,表达式p1<p2等价于p1.compareTo(p2) < 0,其他比较符也是相同的。Comparable和equals一样,也不需要operator操作符

    //实现Comparable接口,Person对象在Kotlin和Java中都能用来比较排序等操作
    //这里先比较Person的firstName,如果firstName相同再比较lastName
    data class Person(
            val firstName: String, val lastName: String
    ) : Comparable<Person> {
        override fun compareTo(other: Person): Int {
            return compareValuesBy(this, other, Person::firstName, Person::lastName)
        }
    }
    val person = Person("Li", "m1Ku")
    val person2 = Person("wang", "rick")
    println(person < person2)
    >> true
    

    compareValuesBy函数可以让你简单方便的实现compareTo方法,这个函数接收要被比价计算值的回调。这个函数会调用每一个回调,并且比较值。如果值不同,那么返回比较结果,如果相同就调用下一个回调或者如果没有更多回调时会返回0。回调可以是lambda表达式或者是属性引用

    3.集合和序列的约定

    ​ 通过索引获取元素或者为集合元素赋值,还有检查一个元素是否属于一个集合都是最常见的集合操作。这些操作都可以使用操作符语句,并且也可以为自己的类定义这些操作符。

    1.通过索引获取元素:get和set

    ​ map元素的取值和赋值都可以通过[]中括号操作符完成

    val params = hashMapOf("name" to "m1ku", "password" to "123456", "token" to "erwer3fg")
    val name = params["name"]
    params["password"] = "654321"
    println(name)
    println(params)
    >> m1Ku
       {name=m1ku, password=654321, token=erwer3fg}
    

    ​ Kotlin中,索引操作符一个约定。使用索引操作符获取一个元素会转换为调用get方法,微元素设置会转化为调用set方法。Map和MutableMap中已经定义了这样的方法。

    ​ 如何在自己的类中定义这样的操作符呢?

    ​ 我们需要做的就是定义一个由operator修饰的名字叫get的函数

    //定义所以操作符函数,获取Point的x和y坐标
    operator fun Point.get(index: Int): Int {
        return when (index) {
            0 -> x
            1 -> y
            else ->
                throw IndexOutOfBoundsException()
        }
    }
    val point = Point(10, 88)
    //调用这个时,转化为调用get函数
    println(point[1])
    >> 88
    

    ​ 定义一个set函数能让我们已类似的方式为集合元素赋值

    operator fun Point.set(index: Int, value: Int) {
        when (index) {
            0 -> x = value
            1 -> y = value
            else ->
                throw IndexOutOfBoundsException()
        }
    }
    val point = Point(10, 88)
    //使用约定语句为元素赋值
    point[0] = 100
    println(point[0])
    >> 100
    

    set函数最后一个元素是赋值运算式右边的值,其他元素是方括号中给定的索引

    2."in"约定

    in操作符:判断一个对象是否属于一个集合,对应调用的函数是contains

    operator fun Rectangle.contains(p: Point): Boolean {
        return p.x in upperLeft.x until lowerRight.x &&
                p.y in upperLeft.y until lowerRight.y
    }
    val rect = Rectangle(Point(10, 20), Point(50, 50))
    println(Point(20, 30) in rect)
    >> true
    

    in右边是调用contains函数的对象,左边是传递给函数的参数

    val point = Point(20,30)
    //下面这两句是等价的
    point in rect
    rect.contains(point)
    

    3.rangeTo约定

    ​ 使用..语句创建一个序列,其实..操作符是一种简单的调用rangeTo函数的方式。可以为自己的类定义一个rangTo函数,但是当实现了comparable接口的类不需要自己自己定义这个函数。Kotlin标准库为实现了comparable接口的类定义了rangeTo方法。

    //Circle实现了comparable接口,可以调用rangeTo函数返回一个序列
    //我们可以判断不同元素是否在序列中
    val startC = Circle(10f)
    val endC = Circle(200f)
    val circle = Circle(5f)
    val circleRange = startC..endC
    println(circle in circleRange)
    >> false
    

    4.for循环的"iterator"约定

    ​ Kotlin的for循环和范围检查使用的都是in操作符,但是在这里的意义是不同的,这里用来执行迭代操作。在Kotlin中这也是一种约定,这意味着iterator方法可以定义为扩展函数。这就是为什么一个普通Java的String也可以进行迭代了:在String的超类CharSequence上定义了iterator扩展函数

    ​ 我们可以为自己的类定义iterator方法

    operator fun ClosedRange<Circle>.iterator(): Iterator<Circle> =
            object : Iterator<Circle> {
                var current = start
                override fun hasNext(): Boolean {
                    return current <= endInclusive
                }
    
                override fun next(): Circle {
                    return current
                }
            }
    

    4.解构声明和组件函数

    ​ 现在已经熟悉了约定的使用,现在看一下数据类的最后一个特点,解构声明。这个特性可以将一个复合值拆开并将其存储在不同的变量中。

    val p = Point(10,20)
    //声明x,y变量,并用p给他们初始化赋值
    val(x,y) = p
    println(x)
    >> 10
    

    ​ 解构声明看起来和普通的变量声明很像,但是解构声明是将一组变量放在括号中。这里解构声明也是用到了约定。对于解构声明中的每一个变量,都会调用一个叫componentN的函数,N是变量声明的位置。

    //上面的解构声明等价于下面两行代码
    x = p.component1()
    y = p.component2()
    

    ​ 对于数据类,编译器为主构造器中声明的每个属性生成了一个componentN函数

    ​ 对于有多个返回值的函数,使用解构声明是很方便的,我们可以将需要返回的值定义在一个类中,然后函数返回这个类,再使用解构声明就方便的获取到了需要的值

    data class NameComponent(val name: String, val extension: String)
    fun splitName(fullName: String): NameComponent {
    
        val result = fullName.split(".")
        return NameComponent(result[0], result[1])
    }
    
    val (name, extension) = splitName("kotlin实战.pdf")
    println("name = $name extension = $extension")
    >> name = kotlin实战 extension = pdf
    

    ​ Kotlin为集合和数组定义了componentN函数,所以集合可以直接使用解构声明。当集合大小已知时,可以简化为

    fun splitName2(fullName: String): NameComponent {
        val (name, extension) = fullName.split(".",limit = 2)
        return NameComponent(name, extension)
    }
    

    ​ Kotlin标准函数库允许我们通过解构声明获得容器中的前5个元素

    1.解构声明和循环

    ​ 解构声明不止可以用在函数的顶层语句中,而且还可以用在其他可以声明变量的地方,比如:循环。

    //遍历一个map
    //这个例子使用了两次约定:迭代对象,解构声明
    fun printEntry(map: Map<String, String>) {
        for ((key, value) in map) {
            println("$key$value")
        }
    }
    

    5.重用属性访问逻辑:委托属性

    委托属性依赖于约定,它是Kotlin一个独特的强有力的特性。这个特性实现的基础是委托:委托是一种设计模式,它可以将一个对象要执行的任务,委托给另一个对象执行。辅助执行的对象叫:委托。当把这种模式使用在属性上时,就可以把访问器的逻辑委托给一个辅助对象。

    1.委托属性的基本操作

    ​ 属性委托的语法如下:

    class Example {
        var p: String by Delegate()
    }
    

    ​ 这里属性p将它的访问器逻辑委托给Delegate类的一个对象,通过关键字by对其后的表达式求值来获取这个对象。根据约定,委托类必须有getValuesetValue方法。像往常一样,他们可以是成员函数也可以是扩展函数。

    ​ 可以把example.p当做普通属性使用,但它将调用Delegate类辅助属性的方法

    //调用委托类的setValue方法
    example.p = "hhahah"
    //调用委托类的getValue方法
    val value = example.p
    

    2.使用委托属性:惰性初始化和 by lazy()

    惰性初始化,是一种常见的模式,直到第一次访问某个属性时,对象的一部分才会按需创建。当初始化过程占据很多的资源,并且当对象使用时这些数据并不会用到时,这种模式是很有用的。

    class Person(val name: String) {
      //使用lazy标准库函数实现委托
        val emails by lazy { loadEmails(this) }
    
        private fun loadEmails(person: Person): List<String> {
            println("初始化函数调用")
            return listOf("1", "2")
        }
    }
    val p = Person("m1Ku")
    //当第一次使用这个属性时,属性才会初始化即惰性初始化
    p.emails
    >> 初始化函数调用
    

    lazy函数返回一个包含适当签名的getValue的方法的对象,所以就可以和by关键字一起使用创建一个委托属性。lazy函数的参数一个lambda,执行初始化值的逻辑。lazy函数默认是线程安全的。

    3.实现委托属性

    class User {
        var age: Int by Delegates.observable(18,
                { property, oldValue, newValue ->
                    println("${property.name} $oldValue $newValue")
                })
    }
    val u1 = User()
    u1.age = 10
    >>age 18 10
    

    Delegates.observable()包含两个参数:初始值和修改处理Handler,每次修改属性值都会调用Handler。

    by函数右边不一定是创建实例。它可以是函数调用,另一个属性,或者其他表达式,只要这个表达式的值是一个对象:编译器可以以正确的类型参数调用getValue和setValue方法。

    4.委托属性的转换规则

    ​ 总结一下委托属性的规则,假设有下面这个有委托属性的类:

    class Foo {
    var c: Type by MyDelegate()
    }
    

    MyDelegate的实例会被保存在一个隐藏属性中,我们用<delegate>代表他。编译器会用一个KProperty类型的对象便是属性,我们且用<property>代表他。编译器生成如下的代码:

    class Foo {
        private val <delegate> = MyDelegate()
        var c: Type
        set(value: Type) = <delegate>.setValue(c, <property>, value)
        get() = <delegate>.getValue(c, <property>)
    }
    

    因此每次获取属性时,其对应的setValuegetValue方法就会调用

    5.在map中存储属性值

    ​ 另一个委托属性能派上用场的地方是:用在一个动态定义属性集的对象上。这样的对象叫做:自订对象(expando objects )。

    class Fruit() {
        val attributes = hashMapOf<String, String>()
    
        fun setAttribute(attrName: String, value: String) {
            attributes[attrName] = value
        }
        //将map作为委托属性
        val name: String by attributes
    }
    

    ​ 可以直接在map后面使用by关键字,这是因为标准库为MapMutableMap接口定义了getValuesetValue的扩展函数,属性的名字自动用在map中的键,属性的值就是其对应的map中的值。

    相关文章

      网友评论

          本文标题:Kotlin学习(7)重载操作符和其他约定

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