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
的函数,当时用+=操作符时就会调用这个函数。minusAssign
,timesAssign
也是类似的。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对其后的表达式求值来获取这个对象。根据约定,委托类必须有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<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>)
}
因此每次获取属性时,其对应的setValue
和getValue
方法就会调用
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关键字,这是因为标准库为Map
和MutableMap
接口定义了getValue
和setValue
的扩展函数,属性的名字自动用在map中的键,属性的值就是其对应的map中的值。
网友评论