¶主要内容
- 操作符重载
- 指名(special-named)函数:convension的一种实现
- 属性委派
类似于Java语言有好几样特性一样,譬如对象实现了java.lang.Iterable
接口的可以使用for
循环,实现了java.lang.AutoCloseable
接口的可以使用try-with-resources语句(Java 8 之后)。Kotlin提供了一种 convertions 技术,实现对操作符的重载。
¶Overloading arithmetic operators
Kotlin中最直接的公约是算术运算符。在Java中,算术运算符仅可以用于原生类型,额外地+
号可以用于连结String
类型。但对于其它类如BigInteger
,使用+
比起使用add
方法会更加直观。
¶Overloading binary arithmetic operations
Kotlin的公约提供了操作符重载,
1 | data class Point(val x: Int, val y: Int) { |
这里用到operator
关键字来声明plus
函数。所有需要重载运算符都需要使用该关键字。表示显式地实现相应的约定。
另外一种写法是结合扩展函数定义,因为很多情况下你并不希望或不能修改原有的类。
1 | operator fun Point.plus(other: Point): Point { |
下标罗列的约定的所有算术符号对应的函数名,
Expression | Function name |
---|---|
a * b |
times |
a / b |
div |
a % b |
mod |
a + b |
plus |
a - b |
minus |
运算符的定义并没有严格要求两个对象的类型必须一致,
1 | operator fun Point.times(scale: Double): Point { |
但是Kotlin的operator不支持交换律 commutativity 。所以你需要重新再定义以满足交换律。operator fun Double.times(p: Point): Point
。
返回类型也可以自定义,
1 | operator fun Char.times(count: Int): String { |
** No special operators for bitwise operations**
Kotlin并没有定义位运算符。相反,位运算被定义为常规的中缀调用语法(infix call syntax)。如,
shl
,左移shr
,右移ushr
,无符号右移and
,按位与or
,按位或xor
,按位异或inv
,按位反譬如,
1
2
3
4
5
6 >>> println(0x0F and 0xF0)
0
>>> println(0x0F or 0xF0)
255
>>> println(0x1 shl 4)
16
¶Overloading compound assignment operators
复合赋值操作符( compound assignment operator ),跟算术操作符一样,
1 | >>> var point = Point(1, 2) |
对于可变集合来说,+=
操作相当于添加新的元素,
1 | >>> val numbers = ArrayList<int>() |
对于可变集合,Kotlin标准库中定义了plusAssign
函数公约,
1 | operator fun <T> MutableCollection<T>.plusAssign(element: T) { |
代码中如果用到+=
,理论上会调用到plus
和plusAssign
函数,
如果定义的类是不可变的,相应定义返回新值的函数plus
;如果是可变的,则定义返回可变实例的plusAssign
函数。但请尽量不要同时都定义这两个函数。
而操作符+
和-
的集合运算总是返回新的集合,其中,对于可变集合,直接修改其值;对于不可变集合,返回新的集合copy实例。
1 | >>> val list = arrayListOf(1, 2) |
¶Overloading unary operators
Kotlin中支持一元操作( unary )。
1 | operator fun Point.unaryMinus(): Point { |
一元操作函数重载不需要任何参数,
列表如下,
Expression | Function name |
---|---|
+a |
unaryPlus |
-a |
unaryMinus |
!a |
not |
++a a++ |
inc |
--a a-- |
dec |
对于自增inc
或自减dec
函数来说,编译器会自动支持前置或后置的语义识别,
1 | operator fun BigDecimal.inc() = this + BigDecimal.ONE |
¶Overloading comparison operators
Kotlin约定函数中也相应定义了比较操作符(==
、!=
、>
、<
等)。
¶Equality operators: “equals”
==
操作在Kotlin中真正被调用为equals
方法。!=
实际上也是被翻译为equlas
的调用,只不过为相反方式。不同的是==
和!=
可以用于对null
的比较,因为它底层可以检测null
值。
¶Ordering operators: compareTo
compareTo
方法被定义在Comparable
接口中。但是Java中只有原生类型采用使用比较操作符>
、<
;其它类型只能显式地使用element1.compareTo(element2)
来比较两个对象。
Kotlin支持同样的Comparable
接口。但是compareTo
方法定义的接口在Kotlin中无法进行重载,因为它底层被翻译为compareTo
的调用,
和equlas
一样,你需要继承Comparable
接口对其方法进行重载,
1 | class Person(val firstName: String, val lastName: String): Comparable<Person> { |
compareValuesBy
为Kotlin的内联函数,该函数接收一系列回调并比较返回值。
¶Conventions used for collections and ranges
¶Accessing elements by index: “get” and “set”
Kotlin约定支持 index operator 下标操作符,语法和Java的数组访问类似,
1 | val value = map[key] |
你也可以直接对其进行赋值操作,
1 | mutableMap[key] = newValue |
Kotlin中的下标操作符,对于读取调用了get
方法,对于写操作调用了set
。该方法在Map
和MutableMap
接口早已定义。你可以在自定义类中实现,
1 | operator fun Point.get(index: Int): Int { // Defines an operator function named "get" |
当调用p[1]
时,将被翻译为get
方法的调用。
需要注意的是,get
方法的参数可以任意,例如你可以实现一个二维数组的下标读operator fun get(rowIndex: Int, colIndex: Int)
,然后通过matrix[row, col]
。
类似地,下标的写操作要求对象可变的,因此你需要声明可变的对象属性,否则更改是无意义的,
1 | data class MutablePoint(var x: Int, var y: Int) |
¶The “in” convention
Kotlin集合中另一个支持的操作符为in
,用于检测对象是否被包含在集合中。对应被调用的函数是contains
。
1 | data class Rectangle(val upperLeft: Point, val lowerRight: Point) |
¶The rangeTo convention
..
操作符对应于rangeTo
函数的调用,用于常见Range
对象的实例,
你可以为自定义类扩展该公约。但如果你的函数继承了Comparable
,则不需要了:因为标准库为任意的Comparable
实例定义了Range的方法。标准库中定义的rangeTo
函数带有泛型参数,
1 | operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T> |
我们以LocalDate
类为例,
1 | >>> val now = LocalDate.now() |
表达式now..now.plusDays(10)
被编译器翻译为now.rangeTo(now.plusDays(10))
。rangeTo
函数并不是LocalDate
的成员,但是Comparable
的扩展函数。
在算术操作符中,rangeTo
操作符的优先级最低,所以最好用括号括起来以避免歧义:
1 | >>> val n = 9 |
注意表达式0..n.forEach {}
不会被编译成功,因为你必须用括号括起来才能作为表达式:
1 | >>> (0..n).forEach { print(it) } // Put a range in parentheses to call a method on it. |
¶The “iterator” convention for the “for” loop
迭代表达式for (x in list) { ... }
编译时被翻译为list.iterator()
,和Java一样,不停地重复调用hasNext
和next
方法。
在Kotlin中它被作为convention约定,意味着iterator
方法可以扩展定义。其中,标准库中就有对字符串的iterator的扩展定义,
1 | operator fun CharSequence.iterator(): CharIterator |
你可以在自己的类定义iterator
方法,例如,
1 | operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> = object : Iterator<LocalDate> { |
¶Destructuring declarations and component functions
destructuring declaractions ,Kotlin的声明解构作为feature实现。
1 | >>> val p = Point(10, 20) |
声明解构在Kotlin中也是作为convension约定,它的底层原理如下,
对于data class
而言,编译器会为每个属性在primary constructor声明的属性创建相应的componentN
函数。
1 | class Point(val x: Int, val y: Int) { |
声明解构的一个好处在于可以从一个函数中返回多个值。
1 | data class NameComponents(val name: String, val extension: String) // Declares a data class to hold the values |
¶Destructuring declarations and loops
声明解构常常用于map的迭代,
1 | fun printEntries(map: Map<String, String>) { |
¶Resuing property accessor logic: delegated properties
delegated properties仅属于Kotlin中的独特的强大的特性。该特性让你以最简单的方式实现属性,让其原本以更复杂的方式存储,但不会重复实现逻辑。例如,属性可以存储在数据库表、浏览器session、map中等。
该特性的基础是委派( delegation ):一种设计模式。
¶Delegated properties: the basics
常规的委派属性语法如下:
1 | class Foo { |
语法格式为 val /var <property name>: <Type> by <expression>
。by
之后的表达式是一个委派,因为属性对应的get()
和set()
会通过约定委派给它的getValue()
和setValue()
方法。属性委派不需要实现接口,但需要提供getValue()
函数(和setValue()
如果是var
可变属性)。
例如,
1 | class Foo { |
按照约定,Delegate
类必须要有getValue
和setValue
方法(setValue
仅为可变属性时被要求)。通常它们可以是成员或扩展。为了简化解析,我们省略它们的参数,Delegate
类看起来会像,
1 | class Delegate { |
foo.p
被作为常规属性使用,但底层调用了帮助类Delegate
的方法调用。Kotlin标准库针对不同的委派模式提供了工厂方法调用。官方之为Standard delegates,包含两部分,lazy properties和observable properties。
¶Using delegated properties: lazy initialization and “by lazy()”
Lazy initialization 仅在对象第一次访问时才被初始化。
例如,假设有一个类Person
允许你访问他的邮箱列表。其中邮箱被存储在数据库中需要花费一段时间才能访问。你希望数据库的访问操作仅一次也第一次属性访问时才触发。
1 | class Email { /*...*/ } |
下面通过额外属性_emails
存储null
方式实现lazy加载的目的,
1 | class Person(val name: String) { |
但这种写法有点啰嗦,如果有好几个lazy properties需要实现会显得繁琐。另外,上述代码也不是线程安全的。
Kotlin标准库提供了by lazy
的函数调用,
1 | class Person(val name: String) { |
lazy
函数返回包含getValue
方法签名的一个对象,因此你可以结合by
关键一起创建委派属性。这里的lazy
的入参是一个lambda用于初始化。lazy
函数默认是线程安全的。
¶Implementing delegated properties
下面以示例方式,阐述委派属性是如何实现的。我们希望实现最常见的事件通知类,
1 | open class PropertyChangeAware { |
编写Person
类,希望属性在发生变更时触发listener,
1 | class Person(val name: String, age: Int, salary: Int): PropertyChangeAware() { |
这里的setter部分代码有许多重复的地方。我们通过委派方式将其分离出单独一个类,
1 | class ObservableProperty(val propName: String, var propValue: Int, val changeSupport: PropertyChangeSupport) { |
现在已经接近理解kotlin的委派属性是如何工作的了。但仍然有少许的样板代码,kotlin的委派属性的特性让你可以移除掉这部分样板代码(boilerplate)。在此之前,你需要按照约定,更改ObservableProperty
相关方法的签名。
1 | class ObservableProperty(var porpValue: Int, val changeSupport: PropertyChangeSupport) { |
对比上一个版本,有如下改变,
getValue
和setValue
函数标记有operator
,按照约定要求。- 这些函数有两个参数:一个是该属性的实例对象,另一个是属性自身。该属性用
KProperty
的一个对象表示。你可以通过KProperty.name
访问属性名。 name
属性从第一构造函数(primary constructor)移除掉了,因为你可以从KProperty
直接访问。
最终的委派属性的使用将变得非常简短,
1 | class Person(val name: String, age: Int, salary: Int): PropertyChangeAware() { |
通过by
关键字,kotlin编译器自动地做了前面版本本应手动要做的部分。对比前面的Person
类:编译器生成的代码非常相似。by
右边的对象被称为 delegate 。kotlin将委派存储在一个隐藏属性,通过委派上的getValue
和setValue
的调用来修改或访问原来的属性。
如其手写这个ObservableProperty
类,不妨使用Kotlin的标准库,
1 | class Person (val name: String, age: Int, salary: Int): PropertyChangeAware() { |
by
右边的表达式不需要创建新的实例。可以是一个函数调用。只要该表达式的值对象是能被编译器调用getValue
和setValue
即可。
¶Delegated-property translation rules
让我们总结下委派属性是如何工作的。假设你的类包含如下委派属性:
1 | class C { |
MyDelegate()
的实例将被存储在一个隐藏属性,我们将其作为 <delegate>
引用。编译器将使用一个KProperty
类型的对象来表示该属性。我们将其作为 <property>
引用。
编译器会生成如下代码,
1 | class C { |
因此,每个属性访问器(property accessor)内部,编译器生成相应的getValue
和setValue
方法进行调用,结构如下,
¶Storing property values in a map
委派属性带来的另外一个常见模式是,对象拥有了动态定义的属性。这类对象有时被称为 expando objects 。例如,假设有一个联系人管理系统存储了联系人的任意信息。系统中的每个人仅包含一少部分属性,也包含有一少部分额外的属性(attribute)(例如,孩提期生日)。
实现该系统的一种方式是将联系人的所有attribute都存储在一个map,并提供访问该信息的对应属性(property)。
1 | class Person { |
简化代码的实现,使用by
关键字委派属性,
1 | class Person { |
这里是可以工作的,因为标准库为Map
和MutableMap
接口提供了getValue
和setValue
的扩展函数。
¶Delegated properties in frameworks
对于一个object内部的property修改也极其简单。假设数据库表Users
包含两列:字符串类型name
、整型age
。Kotlin
的定义如下,
1 | import org.jetbrains.exposed.dao.IntEntity |
框架exposed对于实体类属性(attribute)实现了委派属性,使用了列对象(Users.name, Users.age
)进行委派:
1 | class User(id: EntityID): IntEntity(id) { |
而对于现实指定的数据库列对象,
1 | object Users : IntIdTable() { |
框架底层定义了convention进行委派,
1 | operator fun <T> Column<T>.getValue(o: Entity, desc: KProperty<*>): T { |
你可以使用Column
属性(Users.name
)作为被委派的属性(name
)。
¶Summary
- Kotlin的约定(convention)定义了一些标准函数,可以让你对其进行重载以实现自定义类的operations。
- comparison operator对应会调用到
equals
和compareTo
方法。 - 通过定义
get
、set
和contains
函数,你可以在自定义的类进行类似于集合的[]
和in
的操作。 - Kotlin的约定(convention)也支持集合Range的迭代语法。
- 声明解构(destructuring declarations)可以让你初始化多个值,它的底层也是一种约定,对应调用了代码编译器生成的
componentN
函数。 - 委派属性(delegated property)允许属性部分逻辑的重用,譬如存储、初始化、访问、修改等。
lazy
标准库函数提供了属性的lazily initialized。Delegates.observalbe
函数定义在标准库中,实现了观察者模式。