Kotlin 知識梳理(8) 運算子過載及其他約定

澤毛發表於2017-12-13

一、本文概要

本文是對<<Kotlin in Action>>的學習筆記,如果需要執行相應的程式碼可以訪問線上環境 try.kotlinlang.org,這部分的思維導圖為:

Kotlin 知識梳理(8)   運算子過載及其他約定
Kotlin中,我們可以通過 呼叫自己程式碼中定義的函式,來實現 特定語言結構。這些功能與 特定的函式命名 相關,而不是與特定的型別繫結。例如,如果在你的類中定義了一個名為plus的特殊方法,那麼按照約定,就可以在該類的例項上使用+運算子,這種技術稱為 約定

因為由類實現的介面集是固定的,而Kotlin不能為了實現其他介面而修改現有的類,因此一般 通過擴充套件函式的機制 來為現有的類增添新的 約定方法,從而適應任何現有的Java類。

二、過載算術運算子

Kotlin中,使用約定的最直接的例子就是 算術運算子,在Java中,全套的算術運算子只能用於基本資料型別,+運算子可以與String一起使用。下面,我們看一下在Kotlin中,如何使用算術運算子來完成一些其它的事情。

2.1 過載二元運算子

假設已經有一個資料類Point,它包含兩個成員變數,分別是x,y點的座標值,我們希望通過算術運算子+對兩個Point物件相加之後,能夠得到一個新的Point物件,它的成員變數x,y為原有兩個Point物件的x,y之和。

Kotlin 知識梳理(8)   運算子過載及其他約定
執行結果為:
Kotlin 知識梳理(8)   運算子過載及其他約定
在上面的程式碼中,我們為Point類定義了一個擴充套件函式plus,這樣當我們呼叫first + second,實際上執行的是first.plus(second)方法來得到一個新的Point物件。這裡需要注意的是:用於過載運算子的所有函式都需要 用 operator 關鍵字來標記,用來表示你打算 把這個函式作為相應的約定的實現

所有可過載的二元算術運算子如下,自定義型別的運算子,基本上和標準數字型別的運算子有著相同的優先順序。

  • a * btimes
  • a / bdiv
  • a % bmod
  • a + bplus
  • a - bminus

運算子函式和 Java

  • 當從Java呼叫Kotlin運算子非常容易,只需要像普通函式一樣呼叫即可,例如上面的plus方法。
  • 當從Kotlin呼叫Java的時候,對於與Kotlin約定匹配的函式(不要求使用operator修飾符,但是引數需要匹配名稱和數量)都可以使用運算子語言來呼叫。如果Java類定義了一個滿足需求的函式,但是起了一個不同的名稱,可以通過定義一個擴充套件函式來修正這個函式名用來替代現有的Java方法。

沒有用於位運算的特殊運算子

Kotlin沒有為標準數字型別IntLong等定義任何位運算子,因此也不允許你為自定型別定義它們。相反,它使用中綴呼叫語法的函式,可以為自定義型別定義相似的函式,下面我們為Point新增一個and,用於執行位運算。

Kotlin 知識梳理(8)   運算子過載及其他約定
執行結果為:
Kotlin 知識梳理(8)   運算子過載及其他約定
這裡我們不再使用operator關鍵字來宣告,而是用infix來定義一箇中綴呼叫語法的函式,其它執行位運算的函式包括:shlshrushrandorxorinv

2.2 過載複合賦值運算子

當在定義像plus這樣的函式,Kotlin不止支援+號運算,也支援像+=這樣的 複合賦值運算子

Kotlin 知識梳理(8)   運算子過載及其他約定
需要注意,這個只對於可變變數有效,也就是first要宣告為var。在一些情況下,定義+=運算子可以 修改使用它的變數所引用的物件,但不會重新分配引用,將一個元素新增到可變集合,就是一個很好的例子:

Kotlin 知識梳理(8)   運算子過載及其他約定
如果你定義了一個返回值為Unit,名為plusAssign的函式,Kotlin將會在用到+=運算子的地方使用它,其它二元運算子也有命名相似的對應函式:minusAssigntimesAssign等。

當在程式碼中用到+=的時候,理論上plusplusAssign都可能會被呼叫,如果兩個函式都有定義並且適用,那麼編譯器就會報錯,例如下面這樣的定義:

Kotlin 知識梳理(8)   運算子過載及其他約定
編譯時的錯誤為:
Kotlin 知識梳理(8)   運算子過載及其他約定
解決方法有兩種:

  • 使用 不可變 val 代替可變 var 來修飾first,這樣plus運算子就不再適用。
  • 不要同時為一個類新增plusplusAssign運算。如果一個類是 不可變的,那就應該只提供返回一個新值的運算;如果一個類是 可變的,例如構建器,那麼只需要提供plusAssign和類似的運算子就夠了。

Kotlin的標準庫支援集合的這兩種方法:

  • +-運算子總是返回一個新的集合
  • +=-=運算子用於可變集合時,始終在一個地方修改它們;而它們用於只讀集合時,會返回一個修改過的副本。

作為它們的運算數,可以使用單個元素,也可以使用元素型別一致的其它集合:

Kotlin 知識梳理(8)   運算子過載及其他約定
執行結果為:
Kotlin 知識梳理(8)   運算子過載及其他約定

2.3 過載一元運算子

過載一元運算的過程和前面看到的方式相同:用預先定義的一個名稱來宣告函式,並用修飾符operator標記。下面的例子中過載了-a運算子:

Kotlin 知識梳理(8)   運算子過載及其他約定
執行結果為:
Kotlin 知識梳理(8)   運算子過載及其他約定
所有可過載的一元演算法運算子包括:

  • +aunaryPlus
  • -aunaryMinus
  • !anot
  • ++a/a++inc
  • --a/a--dec

當你定義incdec函式來過載自增和自減的運算子時,編譯器自動支援與普通數字型別的字首、字尾自增運算子相同的語義。例如字尾運算會先返回變數的值,然後才執行++操作。

三、過載比較運算子

與算術運算子一樣,在Kotlin中,可以對任何物件使用比較運算子(==!=><),而不僅僅限於基本資料型別。

3.1 等號運算子,equals

如果在Kotlin中使用==/!=運算子,它將被轉換成equals方法的呼叫,和其他運算子不同的是,==!=可以用於可空運算數,比較a == b會檢查a是否為飛空,如果不是就呼叫a.equals(b),完整的呼叫如下所示:

a?.equals(b) ?: (b == null)
複製程式碼

對於data修飾的資料類,equals的實現將會由編譯器自動生成,如果需要手動實現,可以參考下面的做法:

Kotlin 知識梳理(8)   運算子過載及其他約定

  • 比較是否指向同一物件的引用,如果是,那麼直接返回true
  • 型別如果不同,直接返回false
  • 比較作為判斷依據的欄位

equals函式之所以被標記為override,這是因為這個方法的實現是在Any類中定義的,而operator關鍵字在基本方法中已經標記了。同時,equals不能實現為擴充套件函式,因為繼承自Any類的實現始終優先於擴充套件函式。

3.2 排序運算子 compareTo

Kotlin中,對於實現了Comparable介面中定義的compareTo方法的類可以按約定呼叫,比較運算子<、>、<=、>=的使用將被轉換為compareTocompareTo的返回型別必須為int,也就是說p1 < p2表示式等價於p1.compareTo(p2) < 0

下面,我們定義一個Person類,讓其根據年齡來比較大小:

Kotlin 知識梳理(8)   運算子過載及其他約定
執行結果為:
Kotlin 知識梳理(8)   運算子過載及其他約定
在上面的例子中,我們用到了Kotlin標準庫函式中的compareValuesBy函式來簡潔地實現compareTo方法,這個函式 接收用來計算比較值的一系列回撥,按順序依次呼叫回撥方法,兩兩一組分別做比較:

  • 如果值不同,則返回比較結果
  • 如果相同,則繼續呼叫下一個
  • 如果沒有更多的回撥來呼叫,則返回0

這些回撥函式可以像lambda一樣傳遞,或者像這裡做的一樣,作為屬性引用傳遞。

四、集合與區間的約定

處理集合最常見的操作包含兩種:

  • 通過下標來獲取和設定元素,使用語法a[b],稱為 下標運算子
  • 檢查元素是否屬於當前集合,使用in運算子。

4.1 通過下標來訪問元素:get 和 set

Kotlin中,下標運算子是一種約定,使用下標運算子讀取元素會被轉換為get運算子方法的呼叫,並且寫入元素將呼叫set,下面我們為Point類新增類似的方法:

Kotlin 知識梳理(8)   運算子過載及其他約定
get的引數可以是任何型別,而不止是Int,例如,當你對map使用下標運算子時,引數型別是鍵的型別,它可以是任意型別。還可以定義具有多個引數的get方法,例如如果要實現一個類來表示二維陣列或矩陣,你可以定義一個方法,例如operator fun get(rowIndex : Int, colIndex : Int),然後用matrix[row, col]來呼叫。

下面,我們再來看一下set的約定方法:

Kotlin 知識梳理(8)   運算子過載及其他約定
執行結果為:
Kotlin 知識梳理(8)   運算子過載及其他約定
定義set函式後,就可以在賦值語句中使用下標運算子,set的最後一個引數用來接收賦值語句中(等號)右邊的值,其他引數作為方括號內的下標。

4.2 in 的約定

集合支援的另一個運算子是in運算子,用於檢查某個物件是否屬於集合,相應的函式叫做contains,下面的例子用於判斷某個點是否處於矩形範圍之內:

Kotlin 知識梳理(8)   運算子過載及其他約定
執行結果為:
Kotlin 知識梳理(8)   運算子過載及其他約定

4.3 rangeTo 的約定

要建立一個區間時,使用的是..語法,例如1..10代表所有從110的數字,..運算子是呼叫rangeTo函式的一個簡潔方法。rangeTo返回一個區間,你可以為自己的類定義這個運算子,但是,如果該類實現了Comparable介面,那麼就不需要了,你可以通過Kotlin標準庫建立一個任意可比較元素的區間,這個庫定義了可以用於任何可比較元素的rangeTo函式

operator fun <T : Comparable<T>> T.rangeTo(that : T) : ClosedRange<T>
複製程式碼

這個函式返回一個區間ClosedRanged,可以用來檢測其它一些元素是否屬於它。

作為例子,我們用LocalData來構建一個日期的區間:

Kotlin 知識梳理(8)   運算子過載及其他約定
執行結果為:
Kotlin 知識梳理(8)   運算子過載及其他約定
上面的now..now.plusDays(10)將會被編譯器轉換為now.rangeTo(now.plusDays(10)),它並不是LocalDate的成員函式,而是Comparable的一個擴充套件函式。

4.4 在 "for" 迴圈中使用 "iterator" 的約定

for迴圈中使用in運算子表示 執行迭代操作,諸如for(x in list) { }將被轉換成list.iterator()的呼叫,然後在上面重複呼叫hasNextnext方法。

Kotlin 知識梳理(8)   運算子過載及其他約定
執行結果為:
Kotlin 知識梳理(8)   運算子過載及其他約定
上面用到了 Kotlin 知識梳理(4) - 資料類、類委託 及 object 關鍵字 中介紹的通過object來實現匿名內部類的知識。

五、解構宣告和元件函式

解構宣告的功能允許你展開單個複合值,並使用它來初始化多個單獨的變數。它再次用到了約定的原理,要在解構宣告中初始化每個變數,將呼叫名為componentN的函式,其中N是宣告中變數的位置。

對於資料類,編譯器為每個在主構造方法中宣告的屬性生成一個componentN函式,下面的例子顯示瞭如何手動為非資料類宣告這些功能:

Kotlin 知識梳理(8)   運算子過載及其他約定
執行結果為:
Kotlin 知識梳理(8)   運算子過載及其他約定
解構宣告主要使用場景之一,是從一個函式返回多個值,這個非常有用。如果要這樣做,可以定義一個資料類來儲存返回所需的值,並將它作為函式的返回型別。在呼叫函式之後,可以用解構宣告的方式,來輕鬆的展開它,使用其中的值。

解構宣告不僅可以用作函式中的頂層語句,還可以用在其他可以宣告變數的地方,例如使用in迴圈來列舉map中的條目:

Kotlin 知識梳理(8)   運算子過載及其他約定
執行結果為:
Kotlin 知識梳理(8)   運算子過載及其他約定


更多文章,歡迎訪問我的 Android 知識梳理系列:

相關文章