第七章 运算符重载及其他约定
约定的定义比较模糊,一般说是通过调用自己代码中定义的函数,来实现特定语言结构。
在Kotlin中,这些功能与特定的函数命名相关,而不是与特定的类型绑定。
7.1 重载算术运算符
Kotlin限定了可以重载的运算符,以及需要在类中定义的对应名字的函数。
开发者是不能定义自己的运算符的。
7.1.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)
}
}
//调用
val p1 = Point(10,20)
val p2 = Point(30,40)
println(p1 + p2)
>>>>>>>
Point(x=40,y=60)
- 可重载的二元算术运算符有
| 表达式 | 函数名 |
| :— | :----|
| a*b | times |
| a/b | div |
| a%b | mod |
| a+b | plus |
| a-b | minus |
其有下面几个要点:
1.可以声明为成员函数或扩展函数。
operator fun Point.plus(other:Point):Point{
return Point(x + other.x, y + other.y)
}
2.重载的运算符,基本上和与标准数字类型的运算符有着相同的优先级。例如*、/和%的优先级高于+和-。
3.定义运算符的时候,两个运算数的类型是自由的,可以不同的。
4.返回值也是自由的,可以不同于任一运算数类型。
5.和普通函数一样,可以定义多个同名的,但参数类型不同的运算符函数。
6.Koltin运算符没有自动支持交换性,即交换运算符两边运算数是不支持的。因为其对应的是定义时的类型。
位运算
特别的,Kotlin中位运算是没有特殊运算符的。但它使用支持中缀调用语法的常规函数。
Kotlin提供的用于执行位运算的函数列表:
- shl:带符号左移
- shr:带符号右移
- ushr:无符号右移
- and:按位与
- or:按位或
- xor:按位异或
- inv:按位取反
位运算示例:
println(0x0F and 0xF0)
println(0x0F or 0xF0)
println(0x1 shl 4)
>>>>
0
255
16
7.1.2 重载复合赋值运算符
和二元运算符相似,+=,-=等这些复合赋值运算符也有对应的指定函数,可以被重载。
一般都是对应二元运算符的指定函数再加Assign就是了。如
- +=:plusAssign
- -=:minusAssign
- *=:timesAssign
这里要着重介绍一个问题:这些复合赋值运算符也会调用上节中定义的二元运算符函数。
甚至于,二元运算符函数和复合赋值运算符函数被调用的机会是一样的,而且被要求是唯一的。
例如对于+=,plus和plusAssign都可能被调用。当两者都被定义且适用,编译器会报错。
a += b
//等同于
a = a.plus(b)
//或
a.plusAssign(b)
由此有几点注意:
- val修饰不可变类型的时候,就不能用plusAssign,除非是集合类或者特别定义了函数内容。
- 不可变类一般只提供plus就可以,可变类可以只提供plusAssign。
特别地,Kotlin标准库对于集合的关于运算符的支持:
- +和-:总是返回一个新的集合
- +=和-=:当是可变集合时,始终在一个地方修改它们;当是只读集合时,会返回一个修改过的副本。但在var修饰只读集合后,也是可以使用的。
- 运算数可以是单个元素,也可以是集合。
示例:
//声明:Koltlin标准库为可变集合定义了plusAssign函数
operator fun <T> MutableCollection<T>.plusAssign(element:T){
this.add(element)
}
//调用
val valList = listOf(1,2,3)
var varList = listOf(1,2,3)
val arrayList = arrayListOf(1,2,3)
//valList += 4不允许,会报异常
val newValList = valList + 4
varList += 4
arrayList += 4
println(newValList)
println(varList)
println(arrayList)
>>>>>
[1, 2, 3, 4]
[1, 2, 3, 4]
[1, 2, 3, 4]
7.1.3 重载一元运算符
重载一元运算符和上面章节中的方式是相同的,只是对应函数没有参数而已。
可重载的一元运算符对应函数列表:
- +a:unaryPlus
- -a:unaryMinus
- !a:not
- a++,++a:inc
- a–,–a:dec
特殊需要注意的是自增自减运算符,涉及的运算优先级是和基本数据类型用到的一样。
示例:
//声明
operator fun BigDecimal.inc() = this + BigDecimal.ONE
//调用
var bd = BigDecimal.ZERO
//先引用后计算
println(bd++)
//先计算后引用
println(++bd)
>>>>>
0
2
7.2 重载比较运算符
Kotlin中可以对任意对象使用比较运算符,如==、!=、>、<等。
7.2.1 等号运算符:equals
Kotlin中,==运算符就是调用equals函数。
与此相同,!=运算符也是调用equals函数,只不过结果是相反的。
代码示例:
class Point(val x:Int,val y:Int){
override fun equals(obj:Any?):Boolean{
if(obj === this) return true
if(obj !is Point) return false
return obj.x == x && obj.y == y
}
}
//调用
println(Point(10,20) == Point(10,20))
println(Point(10,20) != Point(5,5))
println(null == Point(10,20))
>>>>>
true
true
false
围绕这个示例,有几点说明:
- 前面章节提过,如果Point类被data修饰为数据类,equals方法会自动生成。
- ==和!=可用于可空运算数。
a == b 等同于 a?.equals(b) ?: (b==null)
- 恒等运算符=检查的是两个参数是否是同一个对象的引用,不能被重载。对应Java中的。
- 这里equals没有operator,但有override修饰。原因是equals是Any中的基本方法,Any中已经已经用operator修饰过了。也就是说明operator修饰符适用于所有实现或重写它的方法。
- equals不能实现为扩展函数,因为继承自Any类的实现始终优先于扩展函数。
7.2.2 排序运算符:compareTo
Kotlin中比较运算符(<、>、<=、>=)的使用将被转换为compareTo函数。
这里有两点注意:
- compareTo函数是在Comparable接口里的,也就是要支持比较运算符,就必须实现Comparable接口。反过来说,实现了Comparable接口的类,就会支持比较运算符。
- compareTo的返回类型是Int。
a>=b 等同于 a.compareTo(b) >= 0
典型示例:
//声明
class Person(val firstName:String,val lastName:String):Comparable<Person>{
override fun compareTo(other:Person):Int{
return compareValuesBy(this,other,Person::lastName,Person::firstName)
}
}
//调用
val p1 = Person("Alice","Smith")
val p2 = Person("Bob","John")
println(p1 < p2)
>>>>>
false
PS:上面示例中的compareTo函数没有operator修饰,因为同equals一样,已经被用在了基类的接口中。
另外,compareValuesBy是Kotlin标准库中的函数,这个函数接受用来计算比较值的一系列回调,按顺序依次调用回调方法,比较后返回结果。
7.3 集合与区间的约定
处理集合最常见的一些操作都支持运算符语法。
7.3.1 通过下标来访问元素:get和set
Kotlin中,下标运算符是一个约定:
- 读取元素会被转换为get运算符方法的调用。
x[a,b] 相当于 x.get(a,b)
- 写入元素调用的是set运算符方法。
x[a,b] = c 相当于 x.set(a,b,c)
- get和set的参数个数和类型都没有限制,且可以重载,合理即可。
- Map和MutableMap已经定义了这些方法。
示例:
//map使用
val value = map[key]
mutableMap[key] = newValue
//自定义类
data class MutablePoint(var x:Int,var y:Int)
operator fun MutablePoint.set(index:Int,value:Int){
when(index){
0-> x = value
1-> y = value
else -> throw IndexOutOfBoundsException("Invalid param")
}
}
operator fun Point.get(index:Int):Int{
return when(index){
0 -> x
1 -> y
else -> throw...
}
}
//调用
val p = MutablePoint(10,20)
println(p[1])
p[1] = 42
println(p[1])
>>>>>>>
20
42
7.3.2 in的约定
Kotlin中,in运算符相应的函数是contains:in 右边的对象将会调用contains函数,in左边的对象将会作为函数入参。
即a in c 相当于 c.contains(a)
示例:
data class Rectangle(val upperLeft:Point,val lowerRight:Point)
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)
println(Point(5,5) in rect)
>>>>>
true
false
7.3.3 rangeTo的约定
Kotlin中,创建一个区间,要用…语法:这个…运算符其实在调用rangeTo函数。
start..end 相当于 start.rangeTo(end)
Kotlin标准库中为Comparable接口已经定义了可以用于任何可比较元素的rangeTo函数,所以如果目标类已经实现了Comparable接口,那么就不需要定义rangeTo函数了
operator fun <T:Comparable<T>> T.rangeTo(that:T):ClosedRange<T>
示例:
val now = LocalDate.now()
val vacation = now..now.plusDays(10)
println(now.plusWeeks(1) in vacation)
>>>>>>
true
两个注意点:
- …运算符的优先级低于算术运算符,但是最好把参数括起来以免混淆
0..(n+1) 等同于 0..n+1
- 必须把区间表达式括起来才能调用它的语法。
(0..n).forEach{ println(it) }
7.3.4 在for循环中使用iterator的约定
Kotlin中,for循环也可以使用in运算符,但这里,in运算符不是调用contains函数,而是调用iterator()函数,在其中重复调用hasNext和next方法。
示例:
operator fun ClosedRange<LocalDate>.iterator():Iterator<LocalDate> =
object:Iterator<LocalDate>{
var current = start
//这里日期用到了compareTo约定
override fun hasNext() = current <= endInclusive
override fun next() = current.apply{
current = plusDays(1)
}
}
//调用
val newYear = LocalDate.ofYearDay(2017,1)
val daysOff = newYear.minusDays(30)..newYear
for(day in daysOff){ println(day) }
7.4 解构声明和组件函数
Kotlin中,解构声明也用到了约定原理:在解构声明中初始化的变量,将调用名为componentN的函数。
其中N是声明中变量的位置。
标准库只允许使用此语法访问一个对象的前五个元素,也就是N的范围是1,2,3,4,5。
伪代码解释
val (a,b) = p
相当于
val a = p.component1()
val b = p.component2()
对于数据类,编译器为每个在主构造方法中声明的属性生成一个componentN函数。
下面示例为非数据类声明这个函数:
class Point(val x:Int,val y:Int){
operator fun component1() = x
operator fun component2() = y
}
数组和集合中定义有componentN函数,使用示例:
data class NameComponents(val name:String,val extension:String)
fun splitFileName(fullName:String):NameComponents{
//返回的数组被解构声明了,name和extension代表数组的前两个元素
val (name,extension) = fullName.split('.',limit=2)
return NameComponents(name,extension)
}
解构声明与Pair和Triple类对比:
- 解构声明优点在于可以不需要定义自己的类型且少写代码
- 两者都可以让一个函数返回多个值。
7.4.1 解构声明和循环
解构声明可以用在in循环。
示例:
fun printEntries(map:Map<String,String>){
for((key,value) in map){
println("$key --> $value")
}
}
//本质是Entry有componentN函数,相当于
for(entry in map.entries){
val key = entry.component1()
val value = entry.component2()
}
7.5 重用属性访问的逻辑:委托属性
Kotlin中最独特和最强大的功能之一:委托属性。
委托是一种设计模式,操作的对象不用自己执行,而是把工作委托给另一个辅助的对象。
本节所述的功能其实原理是很简单的,难点在于随着转换一步步升级,代码逐步简化优化,中间如果有分神或者跳着看,再加上一些直接加入的Kotlin标准库,就容易对在后面的最终结果变得陌生和不理解。
本节建议还是认真阅读,加强理解和掌握原理,不用急于掌握太细节的东西,在以后的代码中使用到了会理解贯通的更好。
7.5.1 委托属性的基本操作
委托属性通过关键字by来用于任何符合属性委托约定规则的对象。
按照约定,约定规则就是相应的委托类具有getValue和setValue方法,其中setValue仅适用于可变属性。
简单示例:
//声明
class Delegeate{
operator fun getValue(...){ ... }
operator fun setValue(...,value:Type){ ... }
}
class Foo{
var p:Type by Delegate()
}
//调用
val foo = Foo()
val oldValue = foo.p
foo.p = newValue
7.5.2 使用委托属性:惰性初始化和by lazy()
惰性初始化:是一种常见的模式,就是直到第一次访问该属性的时候,才根据需要创建对象的一部分。
当初始化过程消耗大量资源并且在使用对象时并不总是需要数据时需要用到。
Kotlin中,标准库函数lazy可用于惰性初始化。
- lazy函数返回一个对象,该对象具有一个名为getValue且签名正确的方法,因此可以把它与by关键字一起使用来创建一个委托属性。
- lazy的参数是一个lambda,可以调用它来初始化这个值。
- 默认情况下,lazy函数式线程安全的。如果需要,也可以设置要使用哪个锁。
简化示例:
//惰性加载
class Person(val name:String){
//_emails用来保存数据,关联委托,通常称为支持属性
private var _emails:List<Email>? = null
val emails:List<Email>
get(){
if(_emails == null){
_emails = loadEmails(this)
}
return _emails!!
}
}
//简化优化
class Person(val name:String){
val emails by lazy { loadEmails(this) }
}
7.5.3 实现委托属性
本小节主要是一个示例代码演进:实现一个对象的属性更改时通知监听器。
Java中有用于此类通知的相应类:PropertyChangeSupport和PropertyChangeEvent类。在下面示例中用到的明白其意思即可。
1.先来不使用委托属性的版本。
open class PropertyChangeAware{
protected val changeSupport = PropertyChangeSupport(this)
fun addPropertyChangeListener(listener:PropertyChangeListener){
changeSupport.addPropertyChangeListener(listener)
}
fun removePropertyChangeListener(listener:PropertyChangeListener){
changeSupport.removePropertyChangeListener(listener)
}
}
class Person(val name:String,age:Int,salary:Int):PropertyChangeAware(){
var age:Int = age
set(newValue){
val oldValue = field
field = newValue
changeSupport.firePropertyChange("age",oldValue,newValue)
}
var salary:Int = salary
set(newValue){
val oldValue = field
field = newValue
changeSupport.firePropertyChange("salary",oldValue,newValue)
}
}
2.setter有很多重复代码,尝试提取一个类
//提取出ObservableProperty类
class ObservableProperty(val propName:String,var propValue:Int,val changeSupport:PropertyChangeSupport){
fun getValue():Int = propValue
fun setValue(newValue:Int){
val oldValue = propValue
propValue = newValue
changeSupport.firePropertyChange(propName,oldValue,newValue)
}
}
//简化Person类
class Person(val name:String,age:Int,salary:Int):PropertyChangeAware(){
val _age = ObservableProperty("age",age,changeSupport)
var age:Int
get() = _age.getValue()
set(value){ _age.setValue(value) }
val _salary = ObservableProperty("salary",salary,changeSupport)
var salary:Int
get() = _salary.getValue()
set(value){ _salary.setValue(value) }
}
3.使用委托属性继续简化
上面示例其实就是委托属性的工作原理了,下面则是符合Kotlin约定的改造。
//改造ObservableProperty类
class ObservableProperty(var propVvalue:Int,val changeSupport:PropertyChangeSupport){
operator fun getValue(p:Person,prop:KProperty<*>):Int = propValue
operator fun setValue(p:Person,prop:KProperty<*>,newValue:Int){
val oldValue = propValue
propVaule = newValue
changeSupport.firePropertyChange(prop.name,oldValue,newValue)
}
}
//简化Person类
class Person(val name:String,age:Int,salary:Int):PropertyChangeAware(){
var age:Int by ObservableProperty(age,changeSupport)
var salary:Int by ObservableProperty(salary,changeSupport)
}
这次的改造的不同在于:
- getValue和setValue标记了operator
- KProperty是一个表示属性本身的类,在Kotlin标准库里,在后面10.2节中会介绍它。现在大概明白意思即可。
- 通过by,Kotlin编译器会自动执行之前版本的代码中手动完成的操作。
4.使用Koltin标准库Delegates
Kotlin标准库里已经包含了类似于ObservableProperty的类
class Person(val name:String,age:Int,salary:Int):PropertyChangeAware(){
private val observer = {
//定义个lambda
prop:KProperty<*>,oldValue:Int,newValue:Int ->
changeSupport.firePropertyChange(prop.name,oldValue,newValue)
}
//Delegates.observable是标准库函数
var age:Int by Delegates.observable(age,observer)
var salary:Int by Delegates.observable(salary,observer)
}
从示例可以看出,by右边的表达式不一定是新创建的实例,也可以是函数调用、表达式等,只要这个返回值是含有getValue和setValue并且参数类型正确的对象就可以。
7.5.4 委托属性的变换规则
总结下委托属性的变换规则,
委托示例:
class C:{
var prop:Type by MyDelegate()
}
val c = C()
编译器生成代码如下:
//<delegate>就是隐藏属性,<property>是KProperty对象
class C{
private val <delegate> = MyDelegate()
var prop:Type
get() = <delegate>.getValue(this,<property>)
set(value:Type) = <delegate>.setValue(this,<property>,value)
}
7.5.5 在map中保存属性值
委托属性的另一种常见用法,是用在有动态定义的属性集的对象中,例如集合。
标准库已经在Map和MutableMap接口上定义了getValue和setValue扩展函数,所以在委托属性时很方便。
示例:
class Person{
private val _attributes = hashMapOf<String.String>()
//给集合赋值函数,和委托属性没关系
fun setAttribute(attrName:String,value:String){
_attributes[attrName] = value
}
//从map手动取值
val name:String
get() = _attributes["name"]!!
//委托属性写法,两者等同
val name:String by _attributes
}
7.5.6 框架中的委托属性
到此委托属性就说明的可以了,本小节没有什么新增的知识,只是一个关于Exposed框架中用到委托属性的示例。
考虑到Exposed框架示例中有关于数据库取用的代码,一般读者并不熟悉Kotlin中的相关类,在看完上面章节的情况下继续看这个示例对理解委托属性并不会产生更多的帮助,反而可能会因为不熟悉具体类而更加迷惑。所以这里就不展示了,感兴趣的可以直接去搜索Exposed框架看看源码和示例。
7.6 小结
略