[Kotlin] 操作符重載及中綴調用

操作符重載其實很有意思!但這個概念卻很少有人知道,使用操作符重載在某種程度上會給代碼的閱讀帶來一定的麻煩。因此,慎用操作符被認爲是一個好習慣。的確,操作符重載是一把雙刃劍,既能削鐵如泥,也能“引火燒身”,這篇文章將從實用的角度來講解操作符重載的基本用法。

支持重載的操作符類型

Kotlin語言支持重載的操作符類型比較多。以最新版本1.2.21爲準,目前支持重載的操作符可以歸納爲以下幾類:

一元操作符

一元前綴操作符

操作符 對應方法
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()

以上三個操作符在日常使用中頻率很高,第一個操作符在基本運算中很少使用,第二個操作符就是常見的取反操作,第三個操作符是邏輯取反操作。接下來,我們使用擴展的方式重載這三個操作符:

/**
 * 一元操作符
 *
 * @author Scott Smith 2018-02-03 14:11
 */
data class Number(var value: Int)

/**
 * 重載一元操作符+,使其對Number中實際數據取絕對值
 */
operator fun Number.unaryPlus(): Number {
    this.value = Math.abs(value)
    return this
}

/**
 * 重載一元操作符-,使其對Number中實際數據取反
 */
operator fun Number.unaryMinus(): Number {
    this.value = -value
    return this
}

/**
 * 這個操作符通常是用於邏輯取反,這裏用一個沒有意義的操作,來模擬重載這個操作符
 * 結果:始終返回Number中實際數據的負值
 */
operator fun Number.not(): Number {
    this.value = -Math.abs(value)
    return this
}

fun main(args: Array<String>) {
    val number = Number(-3)
    println("Number value = ${number.value}")
    println("After unaryPlus: Number value = ${(+number).value}")
    println("After unaryMinus: Number value = ${(-number).value}")

    number.value = Math.abs(number.value)
    println("After unaryNot: Number value = ${(!number).value}")
}

運行上述代碼,將得到如下結果:

Number value = -3
After unaryPlus: Number value = 3
After unaryMinus: Number value = -3
After unaryNot: Number value = -3

自增和自減操作符

操作符 對應方法
a++/++a a.inc()
a–/–a a.dec()

重載這個操作符相對比較難理解,官方文檔有一段簡短的文字解釋,翻譯成代碼可以這樣表示:

// a++
fun increment(a: Int): Int {
  val a0 = a
  a = a + 1
  return a0
}

// ++a
fun increment(a: Int): Int {
  a = a + 1
  return a
}

看懂上面的代碼後,我們換成需要重載的Number類,Kotlin最終會這樣處理:

// Number++
fun increment(number: Number): Number {
  val temp = number
  val result = number.inc()
  return result
}

// Number++
fun increment(number: Number): Number {
  return number.inc()
}

因此,重載Number類自加操作符,我們可以這樣做:

operator fun Number.inc(): Number {
    return Number(this.value + 1)
}

重載自減操作符同理,完整代碼請參考我的Git版本庫:kotlin-samples

二元操作符

算術運算符

操作符 對應方法
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a % b a.rem(b)
a..b a.rangeTo(b)

前5個操作符相對比較好理解,我們以a + b爲例,舉個一個簡單的例子:

// 重載Number類的加法運算符
operator fun Number.plus(value: Int): Number {
    return Number(this.value + value)
}

fun main(args: Array<String>) {
       println((Number(1) + 2))
}
// 輸出結果:
Number value = 3

相對比較難理解的是第六個範圍運算符,這個操作符主要用於生成一段數據範圍。我們認爲Number本身就代表一個整型數字,因此,重載Number是一件有意義的事情。直接看例子:

operator fun Number.rangeTo(to: Number): IntRange {
    return this.value..to.value
}

fun main(args: Array<String>) {
    val startNumber = Number(3)
    val endNumber = Number(9)

    (startNumber..endNumber).forEach {
        println("value = $it")
    }
}

// 運行結果:
value = 3
value = 4
value = 5
value = 6
value = 7
value = 8
value = 9

“In”運算符

操作符 對應方法
a in b b.contains(a)
a !in b !b.contains(a)

這個操作符相對比較好理解,重載這個操作符可以用於判斷某個數據是否在另外一個對象中。我們用一個非常簡單的自定義類來模擬集合操作:

class IntCollection { 
    val intList = ArrayList<Int>()
}

// 重載"in"操作符
operator fun IntCollection.contains(value: Int): Boolean {
    return this.intList.contains(value)
}

fun main(args: Array<String>) {
    val intCollection = IntCollection()
    intCollection.add(1, 2, 3)
    println(3 in intCollection)
}

// 輸出結果:
true

索引訪問運算符

操作符 對應方法
a[i] a.get(i)
a[i, j] a.get(i, j)
a[i_1, …, i_n] a.get(i_1, …, i_n)
a[i] = b a.set(i, b)
a[i, j] = b a.set(i, j, b)
a[i_1, …, i_n] = b a.set(i_1, …, i_n, b)

這個操作符很有意思,例如,如果你要訪問Map中某個數據,通常是這樣的map.get("key"),使用索引運算符你還可以這樣操作:

val value = map["key"]

我們繼續以IntCollection類爲例,嘗試重寫a[i]a[i] = b兩個運算符,其它運算符同理。

// 重載a[i]操作符
operator fun IntCollection.get(index: Int): Int {
    return intList[index]
}

// 重載a[i] = b操作符
operator fun IntCollection.set(index: Int, value: Int) {
    intList[index] = value
}

fun main(args: Array<String>) {
    val intCollection = IntCollection()
    intCollection.add(1, 2, 3)
    println(intCollection[0])

    intCollection[2] = 4
    print(intCollection[2])
}

接下來,我們用索引運算符來做一點更有意思的事情!新建一個普通的KotlinUser

class User(var name: String,
           var age: Int) {

}

使用下面的方式重載索引運算符:

operator fun User.get(key: String): Any? {
    when(key) {
        "name" -> {
            return this.name
        }
        "age" -> {
            return this.age
        }
    }

    return null
}

operator fun User.set(key: String, value:Any?) {
    when(key) {
        "name" -> {
            name = value as? String
        }
        "age" -> {
            age = value as? Int
        }
    }
}

接下來,你會神奇地發現,一個普通的Kotlin類居然也可以使用索引運算符對成員變量進行操作了,是不是很神奇?

fun main(args: Array<String>) {
    val user = User("Scott Smith", 18)
    println(user["name"])
    user["age"] = 22
    println(user["age"])
}

因此,索引運算符不僅僅可以對集合類數據進行操作,對一個普通的Kotlin類也可以發揮同樣的作用。如果你腦洞足夠大,你還可以發現更多更神奇的玩法。

調用操作符

操作符 對應方法
a() a.invoke()
a(i) a.invoke(i)
a(i, j) a.invoke(i, j)
a(i_1, ……, i_n) a.invoke(i_1, ……, i_n)

重載這個操作符並不難,理解它的應用場景卻有一定的難度。爲了理解它的應用場景,我們來舉一個簡單的例子:

class JsonParser {

}

operator fun JsonParser.invoke(json: String): Map<String, Any> {
    val map = Json.parse(json)
    ...
    return map
}

// 可以這樣調用
val parser = JsonParser()
val map = parser("{name: \"Scott Smith\"}")

這裏的調用有點像省略了一個解析Json數據的方法,難道它僅僅就是這個作用嗎?是的,調用操作符其實就這一個作用。如果一個Kotlin類僅僅只有一個方法,直接使用括號調用的確是一個不錯的主意。不過,在使用的時候還是要稍微注意一下,避免出現歧義。

廣義賦值操作符

操作符 對應方法
a += b a.plusAssign(b)
a -= b a.minusAssign(b)
a *= b a.timesAssign(b)
a /= b a.divAssign(b)
a %= b a.remAssign(b)

這個操作符相對比較好理解,我們以Number類爲例,舉一個簡單的例子:

// 廣義賦值運算符
operator fun Number.plusAssign(value: Int) {
    this.value += value
}

fun main(args: Array<String>) {
    val number = Number(1)
    number += 2
    println(number)
}

// 輸出結果:
Number value = 3

相等與不等操作符

操作符 對應方法
a == b a?.equals(b) ?: (b === null)
a != b !(a?.equals(b) ?: (b === null))

重載這個操作符與Java重寫equals方法是一樣的。不過,這裏要注意與Java的區別,在Java端==用於判斷兩個對象是否是同一對象(指針級別)。而在Kotlin語言中,如果我們不做任何處理,==等同於使用Java對象的equals方法判斷兩個對象是否相等。

另外,這裏還有一種特殊情況,如果左值等於null,這個時候a?.equals(b)將返回null值。因此,這裏還增加了?:運算符用於進一步判斷,在這個情況下,當且僅當b === null的時候,a、b纔有可能相等。因此,纔有了上面的對應關係,這裏以User類爲例舉一個簡單的例子:

class User(var name: String?,
           var age: Int?) {

    operator override fun equals(other: Any?): Boolean {
        if(other is User) {
            return (this.name == other.name) && (this.age == other.age)
        }
        return false
    }
}

注意:這裏有一個特殊的地方,與其它操作符不一樣的地方是,如果使用擴展的方式嘗試重載該操作符,將會報錯。因此,如果要重載該操作符,一定要在類中進行重寫。

比較操作符

操作符 對應方法
a > b a.compareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0

比較操作符是一個在日常使用中頻率非常高的操作符,重載這個操作符只需要掌握以上表格中幾個規則即可。我們以Number類爲例舉一個簡單的例子:

operator fun Number.compareTo(number: Number): Int {
    return this.value - number.value
}

屬性委託操作符

屬性委託操作符是一種非常特殊的操作符,其主要用在代理屬性中。關於Kotlin代理的知識,如果你還不瞭解的話,請參考這篇文章
Delegation。這篇文章介紹的相對簡略,後面會出一篇更詳細的文章介紹代理相關的知識。

中綴調用

看到這裏,可能有一些追求更高級玩法的同學會問:Kotlin支持自定義操作符嗎?

答案當然是:不能!不過,別失望,infix也許適合你,它其實可以看做一種自定義操作符的實現。這裏我們對集合List新增一個擴展方法intersection用於獲取兩個集合的交集:

// 獲取兩個集合的交集
fun <E> List<E>.interSection(other: List<E>): List<E> {
    val result = ArrayList<E>()
    forEach {
        if(other.contains(it)) {
            result.add(it)
        }
    }

    return result
}

接下來,我們就可以在List及其子類中使用點語法調用了。但,它看起來仍然不像一個操作符。爲了讓它更像一個操作符,我們繼續做點事情:
* 添加infix關鍵詞
* 將函數名修改爲∩(這是數學上獲取交集的標記符號)
然而,萬萬沒想到,修改完成後居然報錯了。Kotlin並不允許直接使用特殊符號作爲函數名開頭。因此,我們取形近的字母n用於表示函數名:

// 獲取兩個集合的交集
infix fun <E> List<E>.n(other: List<E>): List<E> {
    val result = ArrayList<E>()
    forEach {
        if(other.contains(it)) {
            result.add(it)
        }
    }

    return result
}

接下來,我們就可以這樣調用了val interSection = list1 n list2,怎麼樣?是不是很像自定義了一個獲取交集的操作符n?如果你希望自定義操作符,可以嘗試這麼做。

其實infix的應用場景還不止這些,接下來,我們再用它完成一件更有意思的事情。

在實際項目開發中,數據庫數據到對象的處理是一件繁瑣的過程,最麻煩的地方莫過於思維的轉換。那我們是否可以在代碼中直接使用SQL語句查詢對象數據呢?例如這樣:

val users = Select * from User where age > 18

紙上學來終覺淺,覺知此事需躬行。有了這個idea,接下來,我們就朝着這個目標努力。
一、先聲明一個Sql類,準備如下方法:

   infix fun select(columnBuilder: ColumnBuilder): Sql {

   infix fun from(entityClass: Class<*>): Sql 

   infix fun where(condition: String): Sql 

   fun <T> query(): T 

二、我們的目的是:最終轉換到SQL語句形式。因此,增加如下實現:

class ColumnBuilder(var columns: Array<out String>) {

}

class Sql private constructor() {
    var columns = emptyList<String>()
    var entityClass: Class<*>? = null
    var condition: String? = null

    companion object {
        fun get(): Sql {
            return Sql()
        }
    }

    infix fun select(columnBuilder: ColumnBuilder): Sql {
        this.columns = columnBuilder.columns.asList()
        return this
    }

    infix fun from(entityClass: Class<*>): Sql {
        this.entityClass = entityClass
        return this
    }

    infix fun where(condition: String): Sql {
        this.condition = condition
        return this
    }

    fun <T> query(): T {
        // 此處省略所有條件判斷
        val sqlBuilder = StringBuilder("select ")

        val columnBuilder = StringBuilder("")
        if(columns.size == 1 && columns[0] == "*") {
            columnBuilder.append("*")
        } else {
            columns.forEach {
                columnBuilder.append(it).append(",")
            }
            columnBuilder.delete(columns.size - 1, columns.size)
        }

        val sql = sqlBuilder.append(columnBuilder.toString())
                            .append(" from ${entityClass?.simpleName} where ")
                            .append(condition)
                            .toString()
        println("執行SQL查詢:$sql")

        return execute(sql)
    }

    private fun <T> execute(sql: String): T {
        // 僅僅用於測試
        return Any() as T
    }
}

三、爲了看起來更形似,再增加如下兩個方法:

// 使其看起來像在數據庫作用域中執行
fun database(init: Sql.()->Unit) {
    init.invoke(Sql.get())
}

// 因爲infix限制,參數不能直接使用可變參數。因此,我們增加這個方法使參數組裝看起來更自然
fun columns(vararg columns: String): ColumnBuilder {
    return ColumnBuilder(columns)
}

接下來,就是見證奇蹟的時刻!

fun main(args: Array<String>) {
    database {
        (select (columns("*")) from User::class.java where "age > 18").query()
    }
}

// 輸出結果:
執行SQL查詢:select * from User where age > 18

爲了方便大家查看,我們提取完整執行代碼段與SQL語句對比:

select          *       from User             where  age > 18
select  (columns("*"))  from User::class.java where "age > 18"

神奇嗎?
至此,我們就可以直接在代碼中愉快地使用類似SQL語句的方式進行方法調用了。

總結

本篇文章從操作符重載實用的角度講解了操作符重載的所有相關知識。如文章開頭所說,操作符重載是一把雙刃劍。用得好事半功倍,用不好事倍功半。因此,我給大家的建議是:使用的時候一定要保證能夠自圓其說,簡單來說,就是自然。我認爲相對於古老的語言C++來說,Kotlin語言操作符重載的設計是非常棒的。如果你知道自己在做什麼,我非常推薦你在生產環境中使用操作符重載來簡化操作。

本篇文章例子代碼點這裏:kotlin-samples


我是歐陽鋒,一個熱愛Kotlin語言編程的學生。如果你喜歡我的文章,請在文章下方留下你愛的印記。如果你不喜歡我的文章,請先喜歡上我的文章。然後再留下愛的印記!

下次文章再見,拜拜!


歡迎加入Kotlin交流羣

如果你也喜歡Kotlin語言,歡迎加入我的Kotlin交流羣: 329673958 ,一起來參與Kotlin語言的推廣工作。

掃描下方二維碼,關注歐陽鋒工作室,學更多編程知識

歐陽鋒工作室

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章