Kotlin重载操作符和约定声明规则

一.重载算数运算符

​ 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类定义的算数运算符其优先级和数字运算符是相同的。

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

2.重载复合赋值操作符

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

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

+=运算符重载会定义一个叫plusAssign的函数,他没有返回值,因为调用者会执行自增逻辑:

public inline operator fun <T> MutableCollection<in T>.plusAssign(element: T) {
    this.add(element)
}

minusAssigntimesAssign也是类似的,Kotlin标准库为可变集合定义了plusAssign这一类的函数。

​所以当你在代码中使用+=时,理论上plus和plusAssign都会被调用。我们应该避免同时为+=添加plus和plusAssign操作符。如果调用者是不可变的(用val修饰),你应该只提供plus操作符返回一个全新的变量,如果调用者是可变的(var修饰),你应该只需要提供plusAssign操作符直接去修改调用者的值。另外在集合操作中,+和-不管是否是可变集合都会返回一个新的集合;+=-=用在可变集合时会改变他们的值,使用在只读集合时,会返回一个修改了的拷贝集合。(这意味着只有当可读集合的引用是var才可以使用+=和-=)

3.重载一元操作符

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

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

表达式函数名
+ aunaryPlus
- aunaryMinus
! anot
++ a,a ++inc
-- a,a --dec

+a直接返回a的值;-a直接返回a的负数值;!a如果a是Boolean类型的话就直接返回a的相反布尔值

二.重载比较运算符

​ 正如算数运算符一样,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表达式或者是属性引用

三.集合和序列的约定

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

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
            }
        }

四.解构声明和组件函数

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

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")
    }
}

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

委托属性依赖于约定,它是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中的值

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我们间的空白格

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值