一.重载算数运算符
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
this.add(element)
}
minusAssign,timesAssign也是类似的,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 //实现Comparable接口,Person对象在Kotlin和Java中都能用来比较排序等操作 //这里先比较Person的firstName,如果firstName相同再比较lastName data class Person( val firstName: String, val lastName: String ) : Comparable 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 object : Iterator 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 for ((key, value) in map) { println("$key$value") } } 五.重用属性访问逻辑:委托属性 委托属性依赖于约定,它是Kotlin一个独特的强有力的特性。这个特性实现的基础是委托:委托是一种设计模式,它可以将一个对象要执行的任务,委托给另一个对象执行。辅助执行的对象叫:委托。当把这种模式使用在属性上时,就可以把访问器的逻辑委托给一个辅助对象。 1.委托属性的基本操作 属性委托的语法如下: class Example { var p: String by Delegate() } 这里属性p将它的访问器逻辑委托给Delegate类的一个对象,通过关键字by对其后的表达式求值来获取这个对象。根据约定,委托类必须有getValue和setValue方法。像往常一样,他们可以是成员函数也可以是扩展函数。 可以把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 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的实例会被保存在一个隐藏属性中,我们用 class Foo { private val var c: Type set(value: Type) = get() = } 因此每次获取属性时,其对应的setValue和getValue方法就会调用 5.在map中存储属性值 另一个委托属性能派上用场的地方是:用在一个动态定义属性集的对象上。这样的对象叫做:自订对象(expando objects )。 class Fruit() { val attributes = hashMapOf fun setAttribute(attrName: String, value: String) { attributes[attrName] = value } //将map作为委托属性 val name: String by attributes } 可以直接在map后面使用by关键字,这是因为标准库为Map和MutableMap接口定义了getValue和setValue的扩展函数,属性的名字自动用在map中的键,属性的值就是其对应的map中的值 好文推荐
发表评论