Kotlin入门系列:第六章 运算符重载及其他约定

1 重载算术运算符

1.1 重载二元算术运算

data class Point(val x: Int, val y: Int) {
	operator fun plus(other: Point): Point { // plus方法是约定的运算符"+"
		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)

使用 operator 关键字来声明 plus 函数。用于重载运算符的所有函数都需要用该关键字标记,用来表示你打算把这个函数作为相应的约定的实现,并且不是碰巧地定义一个同名函数。

在使用了 operator 修饰符声明了 plus 函数之后,你就可以直接使用 + 号来求和了。

// 把运算符定义为扩展函数
operator fun Point.plus(other: Point): Point {
	return Point(x + other.x, y + other.y)
}

可重载的二元算符运算符如下表格:

表达式函数名
a * btimes
a / bdiv
a % brem(旧版本为mod,已废弃)
a + bplus
a - bminus

自定义类型的运算符,基本上和与标准数字类型的运算符有着相同的优先级。

当你在定义一个运算符的时候,不要求两个运算数是相同的类型。

operator fun Point.times(scale: Float): Point {
	return Point((x * scale).toInt(), (y * scale).toInt)
}

>>val p = Point(10, 20)
>>println(p * 1.5)

输出:
Point(x=15, y=30)

注意,Kotlin运算符不会自动支持交换性(交换运算符的左右两边)。如果希望用户能够使用 1.5 * p 以外,还能使用 p * 1.5,你需要为它定义一个单独的运算符:operator fun Float.times(p: Point): Point

operator fun Float.times(p: Point): Point {
    return Point((p.x * this).toInt(), (p.y * this).toInt())
}

>>val p = Point(10, 20)
>>println(1.5f * p)

输出:
Point(x=15, y=30)

运算符函数的返回类型也可以不同于任一运算数类型。

operator fun Char.times(count: Int): String {
	return toString().repeat(count)
}

>>println('a' * 3)

输出:
aaa

注意,和普通函数一样,可以重载 operator 函数:可以定义多个同名的,但参数类型不同的方法。

1.2 重载复合赋值运算符

通常情况下,当你在定义像 plus 这样的运算符函数时,Kotlin不止支持 + 号运算,也支持 +=。像 +=-= 等这些运算符被称为复合赋值运算符。

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

>>var point = Point(1, 2)
>>point += Point(3, 4)
>>println(point)

输出:
Point(x=4, y=6)

在一些情况下,定义 += 运算可以修改使用它的变量所引用的对象,但不会重新分配引用。

>>val numbers = ArrayList<Int>()
>>numbers += 42
>>println(numbers[0])

输出:
42

如果你定义了一个返回值为 Unit,名为 plusAssign 的函数,Kotlin将会在用到 += 运算符的地方调用它。其他二元算术运算符也有命名相似的对应函数。

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

当你在代码中用到 += 的时候,理论上 plusplusAssign 都可能被调用。如果在这种情况下,两个函数都有定义且适用,编译器会报错。

一般来说,最好在设计新类时保持(可变性)一致:尽量不要同时给一个类添加 plusplusAssign 运算。像前面的一个示例中的Point,这个类是不可变的,那么就应该只提供返回一个新值(如 plus)的运算。如果一个类是可变的,比如构建器,那么只需要提供 plusAssign 和类似的运算就够了。

// 运算符 += 可以被转换为plus或者plusAssign函数的调用
a += b -> a = a.plus(b)
a += b -> a.plusAssign(b)

Kotlin标准库支持集合的这两种方法。+- 运算符总是返回一个新的集合。+=-= 运算符用于可变集合时,始终就地修改它们;而它们用于只读集合时,会返回一个修改过的副本(这意味着只有当引用只读集合的变量被声明为 var 的时候,才能使用 +=-=)。

>>val list = arrayListOf(1, 2)
>>list += 3
>>val newList = list + listOf(4, 5)
>>println(list)
>>println(newList)

输出:
[1, 2, 3]
[1, 2, 3, 4, 5]

1.3 重载一元运算符

可重载的一元算法的运算符:

表达式函数名
+aunaryPlus
-aunaryMinus
!anot
++a, a++inc
–a, a–dec
operator fun Point.unaryMinus(): Point {
	return Point(-x, -y)
}

>>val p = Point(10, 20)
>>println(-p)

输出:
Point(x=-10, y=-20)

operator fun BigDecimal.inc() = this + BigDecimal.ONE

>>var bd = BigDecimal.ZERO
>>println(bd++)
>>println(++bd)

输出:
0
2

2 重载比较运算符

2.1 等号运算符:“equals”

使用 == 运算符,它将被转换成 equals 方法的调用。

使用 != 运算符也会被转换成 equals 函数的调用,明显的差异在于,它们的结果是相反的。注意,和所有其他运算符不同的是,==!= 可以用于可空运算数,因为这些运算符事实上会检查运算数是否为null。

// 等式校验==被转换成equals函数的调用,以及null的校验
a == b -> a?.equals(b) ?: (b == null)

如果类被标记为数据类(即标记了 data),equals 的实现将会由编译器自动生成。

// 在不是data class数据类情况下,手动实现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(1, 2))

输出:
true
true
false

使用恒等运算符 === 来检查参数与调用 equals 的对象是否相同。恒等运算符与java中的 == 运算符是完全相同的:检查两个参数是否是同一个对象的引用(如果是基本数据类型,检查它们是否是相同的值)。在实现了 equals 之后,通常会使用这个运算符来优化调用代码。注意,=== 运算符不能被重载。

equals 函数之所以被标记为 override,那是因为与其他约定不同的是,这个方法的实现是在 Any 类中定义的(Kotlin中的所有对象都支持等式比较)。这也解释了为什么不需要将它标记为 operator:Any 中的基本方法就已经标记了,而且函数的 operator 修饰符也适用于所有实现或重写它的方法。还要注意,equals 不能实现为扩展函数,因为继承自 Any 类的实现始终优先于扩展函数。

2.2 排序运算符:compareTo

Kotlin支持相同的 Comparable 接口。但是接口中定义的 compareTo 方法可以按约定调用,比较运算符(><<=>=)的使用将被转换成 compareTo

// 两个对象的比较被转换成compareTo的函数调用,然后结果与零比较
a >= b -> a.compareTo(b) >= 0

// 手动实现compareTo
class Person(val firstName: String, val lastName: String): Comparable<Person> {
	override fun compareTo(other: Person): Int {
		// compareValuesBy()接收用来计算比较值的一系列回调,按顺序依次调用回调方法,两两一组分别做比较,并返回结果
		// 如果值不同,则返回比较结果
		// 如果它们相同,则继续调用下一个
		// 如果没有更多回调来调用,则返回0
		return compareValuesBy(this, other, Person::lastName. Person::firstName)
	}
}

>>val p1 = Person("Alice", "Smith")
>>val p2 = Person("Bob", "Johnson")
>>println(p1 < p2)

输出:
false

在java中实现了 Comparable 接口的类,都可以在Kotlin中使用简洁的运算符语法,不用再增加扩展函数:

>>println("abc" < "bac")

输出:
true

3 集合与区间的约定

3.1 通过下标来访问元素:“get"和"set”

// 相当于java中map.get(key)
val value = map[key]

// 相当于java中map.put(key, value)
mutableMap[key] = newValue

在Kotlin中,下标运算符是一个约定。使用下标运算符读取元素会被转换为 get 运算符方法的调用,并且写入元素将调用 set

// 方括号的访问会被转换为get函数的调用
x[a, b] -> x.get(a, b)

data class Point(val x: Int, val y: Int)

operator fun Point.get(index: Int): Int {
	return when(index) {
		0 -> x
		1 -> y
		else -> throw IndexOutOfBoundsException("Invalid corrdinate $index")
	}
}

>>val p = Point(10, 20)
>>println(p[1])

输出:
20

注意,get 的参数可以是任何类型,而不只是 Int。还可以定义具有多个参数的 get 方法。如果需要使用不同的键类型访问集合,也可以使用不同的参数类型定义多个重载的 get 方法。

// 方括号赋值操作将会转换为set函数的调用
x[a, b] = c -> x.set(a, b, c)

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 corrdinate $index")
	}
}

>>val p = MutablePoint(10, 20)
>>p[1] = 42
>>println(p)

输出:
MutablePoint(x=10, y=42)

3.2 "in"的约定

集合支持的另一个运算符是 in 运算符,用于检查某个对象是否属于集合。相应的函数叫做 contains

// in操作将会转换为contains函数的调用
a in c -> c.contains(a)

data class Rectangle(val upperLeft: Point, val lowerRight: Point)

operator fun Rectangle.contains(p: Point): Boolean {
	// until函数构建一个开区间,开区间不包括最后一个点的区间,如[a, b)
	return p.x in upperLeft.x until lowerRight.x &&
		   p.y in upperRight.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

3.3 rangeTo的约定

// ..运算符将被转换为rangeTop函数的调用
start..end -> start.rangeTo(end)

如果该类实现了 Comparable 接口,那么不需要了:你可以通过Kotlin标准库创建一个任意可比较元素的区间,这个库定义了可以用于任何可比较元素的 rangeTo 函数:

>>val now = LocalDate.now()
>>val vacation = now..now.plusDays(10) // now.rangeTo(now.plusDay(10))
>>println(now.plusWeeks(1) in vacation)

输出:
true

rangeTo 运算符的优先级低于算术运算符,但是最好把参数括起来以免混淆:

>>val n = 9
>>println(0..(n + 1))

输出:
0..10

表达式 0..n.forEach{} 不会被编译,因为必须把区间表达式括起来才能调用它的方法:

>>(0..n).forEach { print(it) }

输出:
0123456789

3.4 在"for"循环中使用"iterator"的约定

iterator 方法可以被定义为扩展函数。这解释了为什么可以遍历一个常规的java字符串:标准库已经为CharSequence定义了一个扩展函数iterator,而它是String的父类:

>>for (c in "abc") {}

// 实现自定义迭代器
operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =
    object: Iterator<LocalDate> {
        var current = start

        override fun hasNext() = current <= endInclusive

        override fun next() = current.apply {
            current = plusDays(1)
        }
    }

>>val newYear = LocalDate.ofYearDay(2017, 1)
>>val daysOff = newYear.minusDays(1)..newYear
>>for (dayOff in daysOff) { println(dayOff) }

输出:
2016-12-31
2017-01-01

4 解构声明和组件函数

解构声明,这个功能允许你展开单个复合值,并使用它来初始化多个单独的变量。

data class Point(val x: Int, val y: Int)

>>val p = Point(10, 20)
>>val (x, y) = p
>>println(x)
>>println(y)

输出:
10
20

一个解构声明看起来像一个普通的变量声明,但它在括号中有多个变量。

要在解构声明中初始化每个变量,将调用名为 componentN 的函数,其中 N 是声明中变量的位置。

// 解构声明被转换为componentN函数的调用
val (a, b) = p -> val a = p.component1()
val (a, b) = p -> val b = p.component2()

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

class Point(val x: Int, val y: Int) {
	operator fun component1() = x
	operator fun component2() = y
}

解构声明主要使用场景之一,是从一个函数返回多个值,这个非常有用。如果要这样做,可以定义一个数据类来保存返回所需的值,并将它作为函数的返回类型。在调用函数后,可以用解构声明的方式,来轻松地展开它,使用其中的值。

// 使用解构声明返回多个值
data class NameComponents(val name: String, val extension: String) {
    companion object {
        fun splitFilename(fullName: String): NameComponents {
            val result = fullName.split(".", limit = 2)
            return NameComponents(result[0], result[1])
        }
    }
}

>>val (name, ext) = NameComponents.splitFilename("example.kt")
>>println(name)
>>println(ext)

输出:
example
kt

不可能无限数量的 componentN 函数,标准库只允许使用此语法来访问一个对象的前五个元素。

4.1 解构声明和循环

// 用解构声明来遍历map
fun printEntries(map: Map<Stirng, Stirng>) {
	for ((key, value) in map) {
		println("$key -> $value")
	}
}

>>val map = mapOf("Oracle" to "Java", "JetBrains" to "Kotlin")
>>printEntries(map)

输出:
Orcale -> Java
JetBrains -> Kotlin

Kotlin标准库给map增加了一个扩展的iterator函数,用来返回map条目的迭代器。因此,与java不同的是,可以直接迭代map。它还包含Map.Entry上的扩展函数component1和component2,分别返回它的键和值。

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

5.1 委托属性的基本操作

// 伪代码
class Foo {
	private val delegate = Delegate()
	var p: Type
		set(value: Type) = delegate.setValue(..., value)
		get() = delegate.getValue(...)
}

按照约定,Delegate 类必须具有 getValuesetValue() 方法。

class Delegate {
	// 包含getter的实现逻辑
	operator fun getValue(...) { ... }

	// 包含setter的实现逻辑
	operator fun setValue(..., value: Type) { ... }
}

class Foo {
	// 关键字by把属性关联上委托对象
	var p: Type by Delegate()
}

>>val foo = Foo()
>>val oldValue = foo.p // 访问Foo对象的属性p,实际调用的是Delegate.getValue()
>>foo.p = newValue // 实际调用的是Delegate.setValue()

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

惰性初始化 是一种常见的模式,直到在第一次访问该属性的时候,才根据需要创建对象的一部分。

class Email { /*...*/ }
fun loadEmails(person: Person): List<Email> {
	println("load emails for ${person.name}")
	return listOf(/*...*/)
}

class Person(val name: String) {
	private var _emails: List<Email>? = null

	val emails: List<Emial>
		get() {
			if (_email == null) {
				_emails = loadEmails(this)
			}
			return _emails!!
		}
}

>>val p = Person("Alice")
>>p.emails // 第一次访问会加载邮件loadEmails()
>>p.emails

这里使用了所谓的 支持属性技术。你有一个属性_emails用来存储这个值,而另一个emails,用来提供对属性的读取访问。你需要使用两个属性,因为属性具有不同的类型:_emails可以为空,而emails为非空。

class Person(val name: String) {
	val emails by lazy { loadEmails(this) }
}

lazy 函数返回一个对象,该对象具有一个名为 getValue 且签名正确的方法,因此可以把它与 by 关键字一起使用来创建一个委托属性。lazy 的参数是一个lambda,可以调用它来初始化这个值。默认情况下,lazy 函数是线程安全的,如果需要,可以设置其他选项来告诉它要使用哪个锁,或者完全避开同步,如果该类永远不会在多线程环境中使用。

5.3 委托属性的变换规则

总结委托属性的工作原理:

class C {
	var prop: Type by MyDelegate()
}
val c = C()

MyDelegate 实例会被保存到一个隐藏的属性中,它被称为 <delegate>。编译器也将用一个 KProperty 类型的对象来代表这个属性,它被称为 <property>

class C {
	private val <delegate> = MyDelegate()

	var prop: Type
		get() = <delegate>.getValue(this, <Property>)
		set(value: Type) = <delegate>.setValue(this, <property>, value)
}

// 当访问属性时,会调用<delegate>的getValue和setValue
val x = c.prop -> val x = <delegate>.getValue(c, <property>)
c.prop = x -> <delegate>.setValue(c, <property>, x)

5.5 在map中保存属性值

委托属性发挥作用的另一种常见用法,是用在有动态定义的属性集的对象中。这样的对象有时被称为自订(expando)对象。

class Person {
	private val _attributes = hashMapOf<String, String>()
	fun setAttribute(attrName: String, value: String) {
		_attributes[attrName] = value
	}

	val name: String
		get() = _attributes["name"]!!
}

>>val p = Person()
>>val data = mapOf("name" to "Dmitry", "company" to "JetBrains")
>>for ((attrName, value) in data) p.setAttribute(attrName, value)
>>println(p.name)

输出:
Dmitry

可以直接将map放在关键字 by 后面:

class Person {
	private val _attributes = hashMapOf<String, String>()
	fun setAttribute(attrName: String, value: String) {
		_attributes[attrName] = value
	}

	// 因为标准库已经在标准Map和MutableMap接口上定义了getValue和setValue扩展函数,所以这里可以直接这样用
	val name: String by _attributes 
}

6 中缀表达式

运算符的重载有一个特点,就是它必须是 kotlin 语法中已经定义好的运算符,不能凭空重载一个运算符,或许在后续的 kotlin 迭代更新会有更多的运算符可以被重载,所以这就有一个限制:运算符是有上限的。为了解决该问题,所以就有了中缀表达式扩展运算符。

中缀表达式通过关键字 infix 声明函数:

sealed class CompareResult {
	object LESS : CompareResult() {
		override fun toString(): String {
			return "小于"
		}
	}
	
	object MORE : CompareResult() {
		override fun toString(): String {
			return "大于"
		}
	}

	object EQUAL : CompareResult() {
		override fun toString(): String {
			return "等于"
		}
	}
}	

// infix 关键字声明一个中缀表达式
infix fun Int.vs(num: Int): CompareResult =
		if (this - num < 0) {
			CompareResult.LESS
		} else if (this - num > 0) {
			CompareResult.MORE
		} else {
			CompareResult.EQUAL
		}

infix fun Double.vs(num: Int): CompareResult =
		if (this - num < 0) {
			CompareResult.LESS
		} else if (this - num > 0) {
			CompareResult.MORE
		} else {
			CompareResult.EQUAL
		}

infix fun Long.vs(num: Int): CompareResult =
		if (this - num < 0) {
			CompareResult.LESS
		} else if (this - num > 0) {
			CompareResult.MORE
		} else {
			CompareResult.EQUAL
		}

fun main(args: Array<String>) {
	println(5 vs 6) // 输出 小于
	// println(5.vs(6))
}

一个函数只有用于两个角色类似的对象时才将其声明为中缀函数,如果一个方法会改动其接收者,那么不要声明为中缀形式。例如 and、to、zip 可以声明为中缀形式,add 会改动接收者所以不建议声明为中缀形式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值