2019/04/08

Kotlin Operator Overloading and other conventions

kotlin 提供 operator overloading 機制,可以覆寫 operator 的行為,例如提供了 plus method,就可以將物件以 + 進行運算。

Overloading arithmetic operators

overloading binary arithmetic operations

a + b -> a.plus(b)

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

    // 覆寫 plus 方法
    operator fun plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }
}

fun main(args: Array<String>) {
    val p1 = Point(10, 20)
    val p2 = Point(30, 40)
    
    // + 就是呼叫 plus
    println(p1 + p2)
}
Expression function name
a*b times
a/b div
a%b mod
a+b plus
a-b minus

如果是 Java 的 class,也可以用 operator fun 的方式定義 plus

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

// extension function,附加到 Point
operator fun Point.plus(other: Point): Point {
    return Point(x + other.x, y + other.y)
}

fun main(args: Array<String>) {
    val p1 = Point(10, 20)
    val p2 = Point(30, 40)
    println(p1 + p2)
}

兩個 operands 不需要有相同的 type,Point 可以乘上 Double

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

// 兩個 operands 不需要有相同的 type
operator fun Point.times(scale: Double): Point {
    return Point((x * scale).toInt(), (y * scale).toInt())
}

fun main(args: Array<String>) {
    val p = Point(10, 20)
    println(p * 1.5)
}

可改變 return data type

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

fun main(args: Array<String>) {
    println('a' * 3)
}

kotlin 沒有定義 bitwise operators,改以 function 並用 index call syntax 處理

function meaning
shl signed shift left
shr signed shift right
ushr unsigned shift right
and
or
xor
inv bitwise inversion
fun main(args: Array<String>) {
    println(0x0F and 0xF0)
    println(0x0F or 0xF0)
    println(0x1 shl 4)
}
Overloading compound assignment operators

除了 + 也能使用 +=

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

operator fun Point.plus(other: Point): Point {
    return Point(x + other.x, y + other.y)
}

fun main(args: Array<String>) {
    var point = Point(1, 2)
    point += Point(3, 4)
    println(point)
}

有時只需要用 += 不需要 +,可以提供 plusAssign 另外加上 Unit return type,類似的 method 為 minusAssign, timesAssign

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

如果在程式裡面呼叫 a+=b,其實會先呼叫 a = a.plus(b) 然後是 a.plusAssign(b)


standard libary 的 +, - 都會產生新的 collection,因此 +=, -= 可同時用在 read-only, mutable collection

fun main(args: Array<String>) {
    val list = arrayListOf(1, 2)
    /// 改變了原本的 list
    list += 3
    
    // 產生新的 list,但原本的 list 不變
    val newList = list + listOf(4, 5)
    println(list)
    println(newList)
}
Overloading unary operators

+a 就是 a.unaryPlus(), -a 是 a.unaryMinus

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

operator fun Point.unaryMinus(): Point {
    return Point(-x, -y)
}

fun main(args: Array<String>) {
    val p = Point(10, 20)
    println(-p)
}
expression function name
+a unaryPlus
-a unaryMinus
!a not
++a, a++ inc
--a, a-- dec
import java.math.BigDecimal

// 覆寫 ++
operator fun BigDecimal.inc() = this + BigDecimal.ONE

fun main(args: Array<String>) {
    var bd = BigDecimal.ZERO
    
    // 在 println 後,呼叫 inc
    println(bd++)
    // 在 println 前,呼叫 inc
    println(++bd)
}

Overloading comparison operators

comparison operators: ==, !=, >, <

euqals ==

a==b 會轉換為 equals 及 null check a?.equals(b) ?: b ==null

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
    }
}

fun main(args: Array<String>) {
    println(Point(10, 20) == Point(10, 20))
    println(Point(10, 20) != Point(5, 5))
    println(null == Point(1, 2))
}
Ordering operators: compareTo

java 的 class 實作 Comparable 介面,就可以比較大小

kotlin 也有支援 Comparable 介面,但是 compareTo method 是用在 call by convention

a>=b 會轉換為 a.compareTo(b) >=0

import kotlin.comparisons.compareValuesBy

class Person(
        val firstName: String, val lastName: String
) : Comparable<Person> {

    override fun compareTo(other: Person): Int {
        return compareValuesBy(this, other,
            Person::lastName, Person::firstName)
    }
}

fun main(args: Array<String>) {
    val p1 = Person("Alice", "Smith")
    val p2 = Person("Bob", "Johnson")
    println(p1 < p2)
}

Conventions used for collections and ranges

Accessing elements by index: get and set

kotlin 要使用 map,是用 map[index] 取得某個 element,類似 java 的 array

如果定義 get function,並標記為 operator fun,就可以用 [] 呼叫 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 coordinate $index")
    }
}

fun main(args: Array<String>) {
    val p = Point(10, 20)
    println(p[1])
}

如果定義 operator fun get(rowIndex: Int, colIndex: Int) 就可以用 matrix[row, col]


set 跟 get 類似

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

fun main(args: Array<String>) {
    val p = MutablePoint(10, 20)
    p[1] = 42
    println(p)
}
"in" convention

in 是呼叫 contains

a in c -> c.contains(a)

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

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
}

fun main(args: Array<String>) {
    val rect = Rectangle(Point(10, 20), Point(50, 50))
    println(Point(20, 30) in rect)
    println(Point(5, 5) in rect)
}
rangeTo

start..end 轉換為 start.rangeTo(end)

standard library 有定義一個 rangeTo,可被任何 comparable element 呼叫

operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T>
import java.time.LocalDate

fun main(args: Array<String>) {
    // 產生十天
    val now = LocalDate.now()
    val vacation = now..now.plusDays(10)
    
    val n = 9
    // 0..10
    println(0..(n + 1))

    // 加上 () 再呼叫 forEach
    (0..n).forEach { print(it) }
}
"iterator" convention for "for" loop

iterator method

import java.util.Date
import java.time.LocalDate

operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =
        // 實作 Iterator
        object : Iterator<LocalDate> {
            var current = start

            // 使用 compareTo convention
            override fun hasNext() =
                    current <= endInclusive

            // 修改前會回傳 current date
            override fun next() = current.apply {
                current = plusDays(1)
            }
        }

fun main(args: Array<String>) {
    val newYear = LocalDate.ofYearDay(2017, 1)
    val daysOff = newYear.minusDays(1)..newYear
    for (dayOff in daysOff) {
        println(dayOff)
    }
    
    //2016-12-31
    //2017-01-01
}

Destructuring declarations and component functions

destructuring declarations 可以 unpacke composite values,儲存在不同的變數

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

val (x, y) = p 等同

val a = p.component1()
val b = p.component2()

data class 會自動為每一個 property 產生 componentN 的 function

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

但也能自己處理

import java.io.File

data class NameComponents(val name: String,
                          val extension: String)

fun splitFilename(fullName: String): NameComponents {
    val result = fullName.split('.', limit = 2)
    return NameComponents(result[0], result[1])
}

fun main(args: Array<String>) {
    val (name, ext) = splitFilename("example.kt")
    println(name)
    println(ext)
}

splitFilename 也可以寫成

fun splitFilename(fullName: String): NameComponents {
    val (name, extension) = fullName.split('.', limit = 2)
    return NameComponents(name, extension)
}

for ((key, value) in map) 可 iterate map

fun printEntries(map: Map<String, String>) {
    for ((key, value) in map) {
        println("$key -> $value")
    }
}

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

Reusing property accessor logic: delegated properties

delegated properties 可實作更複雜的 perperties

Delegated properties: the basics

delegated property 語法如下: p delegates the logic to an instance of Delegate class

class Foo {
    var p: Type by Delegate()
}

實際上 compiler 會產生以下的 code

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

搭配 Delegate 的實作,完整的範例如下

class Delegate {
    operator fun getValue(...) { ... }
    operator fun setValue(..., value: Type) { ... }
}

class Foo {
    var p: Type by Delegate()
}

>>> val foo = Foo()
// 使用 property,會呼叫 delegate.get
>>> val oldValue = foo.p
// 修改 property value 會呼叫 delegate.set
>>> foo.p = newValue
使用 delegated properties: lazy initialization and by lazy()

lazy initialization

例如 Person 可提供多個 emails,但emails 存在 DB 裡面,需要一段時間才能取得,我們需要在第一次使用 emails 時,載入 email 並將 email 暫存。

使用 backing property _emails 的方式,提供這樣的功能

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<Email>
       get() {
           if (_emails == null) {
               _emails = loadEmails(this)
           }
           return _emails!!
       }
}

fun main(args: Array<String>) {
    val p = Person("Alice")
    p.emails
    p.emails
}

如果用 lazy initialization 的方式

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

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

fun main(args: Array<String>) {
    val p = Person("Alice")
    p.emails
    p.emails
}
implementing delegated properties

notifying listeners when a property of an object changes

Java 提供 PropertyChangeSupport, PropertyChangeEvent classes

在 kotlin 使用 PropertyChangeSupport, PropertyChangeEvent 的範例

import java.beans.PropertyChangeSupport
import java.beans.PropertyChangeListener

open class PropertyChangeAware {
    // 儲存 list of listeners, dispatch PropertyChangeEvent events to them
    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) {
            // field 可取得 property backing field
            val oldValue = field
            field = newValue
            // 通知 listeners
            changeSupport.firePropertyChange(
                    "age", oldValue, newValue)
        }

    var salary: Int = salary
        set(newValue) {
            val oldValue = field
            field = newValue
            changeSupport.firePropertyChange(
                    "salary", oldValue, newValue)
        }
}

fun main(args: Array<String>) {
    val p = Person("Dmitry", 34, 2000)
    // 將 property change listener 加入 p
    p.addPropertyChangeListener(
        PropertyChangeListener { event ->
            println("Property ${event.propertyName} changed " +
                    "from ${event.oldValue} to ${event.newValue}")
        }
    )
    p.age = 35
    p.salary = 2100
}

以 ObservableProperty 修改

為每一個 property 都產生 ObservableProperty instance,並 delegate getter, setter

import java.beans.PropertyChangeSupport
import java.beans.PropertyChangeListener

open class PropertyChangeAware {
    protected val changeSupport = PropertyChangeSupport(this)

    fun addPropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.addPropertyChangeListener(listener)
    }

    fun removePropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.removePropertyChangeListener(listener)
    }
}

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)
    }
}

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) }
}

fun main(args: Array<String>) {
    val p = Person("Dmitry", 34, 2000)
    p.addPropertyChangeListener(
        PropertyChangeListener { event ->
            println("Property ${event.propertyName} changed " +
                    "from ${event.oldValue} to ${event.newValue}")
        }
    )
    p.age = 35
    p.salary = 2100
}

調整 ObservableProperty,將 getValue, setValue 改為 operator fun

去掉 name property,改為使用 KProperty.name

class ObservableProperty(
    var propValue: 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
        propValue = newValue
        changeSupport.firePropertyChange(prop.name, oldValue, newValue)
    }
}

最精簡的版本

import java.beans.PropertyChangeSupport
import java.beans.PropertyChangeListener
import kotlin.properties.Delegates
import kotlin.reflect.KProperty

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() {

    private val observer = {
        prop: KProperty<*>, oldValue: Int, newValue: Int ->
        changeSupport.firePropertyChange(prop.name, oldValue, newValue)
    }

    var age: Int by Delegates.observable(age, observer)
    var salary: Int by Delegates.observable(salary, observer)
}

fun main(args: Array<String>) {
    val p = Person("Dmitry", 34, 2000)
    p.addPropertyChangeListener(
        PropertyChangeListener { event ->
            println("Property ${event.propertyName} changed " +
                    "from ${event.oldValue} to ${event.newValue}")
        }
    )
    p.age = 35
    p.salary = 2100
}
Delegated-property translation rules

整理 delegated properties 的運作方式

以下是 class with delegated property

class Foo {
    var c: Type by MyDelegate()
}
val foo = Foo()

實際上 compiler 會產生以下的 code

class Foo {
    private val <delegate> = MyDelegate()
    var c: Type
        set(value: Type) = <delegate>.setValue(c, <property>, value)
        get() = <delegate>.getValue(c, <property>)
}

使用 property 時,會呼叫

val x=c.prop 就是 val x = <delegate>.getValue(c, <property>)

c.prop=x 就是 <delegate>.setValue(c, <property>, x)

Storing property values in a map

expando objects: 有動態定義的 set of attributes

Person 沒有固定的 properties,將 property 放在 hashMap 裡面

class Person {
    private val _attributes = hashMapOf<String, String>()

    fun setAttribute(attrName: String, value: String) {
        _attributes[attrName] = value
    }

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

fun main(args: Array<String>) {
    val p = Person()
    val data = mapOf("name" to "Dmitry", "company" to "JetBrains")
    for ((attrName, value) in data)
       p.setAttribute(attrName, value)
    println(p.name)
}

可以使用 delegated property

class Person {
    private val _attributes = hashMapOf<String, String>()

    fun setAttribute(attrName: String, value: String) {
        _attributes[attrName] = value
    }

    val name: String by _attributes
}

Delegated properties in frameworks

Database framework

// 將 object 關聯至 table in the database
object Users : IdTable() {
    // property 為 columns
    val name = varchar("name", length = 50).index()
    val age = integer("age")
}

// 每一個 User instance 都有特定的 id
class User(id: EntityID) : Entity(id) {
    // name 對應到 DB 的 name
    var name: String by Users.name
    var age: Int by Users.age
}

另一種方式,是定義 Column 類別

object Users : IdTable() {
    val name: Column<String> = varchar("name", 50).index()
    val age: Column<Int> = integer("age")
}

framework 為 Column class 定義了 getValue, setValue

operator fun <T> Column<T>.getValue(o: Entity, desc: KProperty<*>): T {
    // retrieve the value from the database
}
operator fun <T> Column<T>.setValue(o: Entity, desc: KProperty<*>, value: T) {
    // update the value in the database
}

References

Kotlin in Action

沒有留言:

張貼留言