Kotlin 語言學習(8) - 運算符重載及其他約定

Kotlin 語言學習(1) - Kotlin 基礎

Kotlin 語言學習(2) - 函數的定義與調用

Kotlin 語言學習(3) - 類、對象和接口

Kotlin 語言學習(4) - 數據類、類委託 及 object 關鍵字

Kotlin 語言學習(5) - lambda 表達式和成員引用

Kotlin 語言學習(6) - Kotlin 的可空性

Kotlin 語言學習(7) - Kotlin 的類型系統

Kotlin 語言學習(8) - 運算符重載及其他約定

Kotlin 語言學習(9) - 委託屬性

Kotlin 語言學習(10) - 高階函數:Lambda

Kotlin 語言學習(11) - 內聯函數

Kotlin 語言學習(12) - 泛型類型參數

一、本文概要

Kotlin中,我們可以通過 調用自己代碼中定義的函數,來實現 特定語言結構。這些功能與 特定的函數命名 相關,而不是與特定的類型綁定。例如,如果在你的類中定義了一個名爲plus的特殊方法,那麼按照約定,就可以在該類的實例上使用+運算符,這種技術稱爲 約定

因爲由類實現的接口集是固定的,而Kotlin不能爲了實現其他接口而修改現有的類,因此一般 通過擴展函數的機制 來爲現有的類增添新的 約定方法,從而適應任何現有的Java類。

二、重載算術運算符

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

2.1 重載二元運算符

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

運行結果爲:

在上面的代碼中,我們爲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,用於執行位運算。

運行結果爲:

這裏我們不再使用operator關鍵字來聲明,而是用infix來定義一箇中綴調用語法的函數,其它執行位運算的函數包括:shlshrushrandorxorinv

2.2 重載複合賦值運算符

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

需要注意,這個只對於可變變量有效,也就是first要聲明爲var。在一些情況下,定義+=運算符可以 修改使用它的變量所引用的對象,但不會重新分配引用,將一個元素添加到可變集合,就是一個很好的例子:

如果你定義了一個返回值爲Unit,名爲plusAssign的函數,Kotlin將會在用到+=運算符的地方使用它,其它二元運算符也有命名相似的對應函數:minusAssigntimesAssign等。

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

編譯時的錯誤爲:

解決方法有兩種:

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

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

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

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

運行結果爲:

2.3 重載一元運算符

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

運行結果爲:

所有可重載的一元算法運算符包括:

  • +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的實現將會由編譯器自動生成,如果需要手動實現,可以參考下面的做法:

  • 比較是否指向同一對象的引用,如果是,那麼直接返回true
  • 類型如果不同,直接返回false
  • 比較作爲判斷依據的字段

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

Kotlin中,對於實現了Comparable接口中定義的compareTo方法的類可以按約定調用,比較運算符<、>、<=、>=的使用將被轉換爲compareTocompareTo的返回類型必須爲int也就是說p1 < p2表達式等價於p1.compareTo(p2) < 0

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

運行結果爲:

在上面的例子中,我們用到了Kotlin標準庫函數中的compareValuesBy函數來簡潔地實現compareTo方法,這個函數 接收用來計算比較值的一系列回調,按順序依次調用回調方法,兩兩一組分別做比較:

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

這些回調函數可以像lambda一樣傳遞,或者像這裏做的一樣,作爲屬性引用傳遞。

四、集合與區間的約定

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

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

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

Kotlin中,下標運算符是一種約定,使用下標運算符讀取元素會被轉換爲get運算符方法的調用,並且寫入元素將調用set,下面我們爲Point類添加類似的方法:

get的參數可以是任何類型,而不止是Int,例如,當你對map使用下標運算符時,參數類型是鍵的類型,它可以是任意類型。還可以定義具有多個參數的get方法,例如如果要實現一個類來表示二維數組或矩陣,你可以定義一個方法,例如operator fun get(rowIndex : Int, colIndex : Int),然後用matrix[row, col]來調用。

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

運行結果爲:

定義set函數後,就可以在賦值語句中使用下標運算符,set的最後一個參數用來接收賦值語句中(等號)右邊的值,其他參數作爲方括號內的下標。

4.2 in 的約定

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

運行結果爲:

4.3 rangeTo 的約定

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

operator fun <T : Comparable<T>> T.rangeTo(that : T) : ClosedRange<T>

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

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

運行結果爲:

上面的now..now.plusDays(10)將會被編譯器轉換爲now.rangeTo(now.plusDays(10)),它並不是LocalDate的成員函數,而是Comparable的一個擴展函數。

4.4 在 "for" 循環中使用 "iterator" 的約定

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

運行結果爲:

五、解構聲明和組件函數

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

對於數據類,編譯器爲每個在主構造方法中聲明的屬性生成一個componentN函數,下面的例子顯示瞭如何手動爲非數據類聲明這些功能:

運行結果爲:

解構聲明主要使用場景之一,是從一個函數返回多個值,這個非常有用。如果要這樣做,可以定義一個數據類來保存返回所需的值,並將它作爲函數的返回類型。在調用函數之後,可以用解構聲明的方式,來輕鬆的展開它,使用其中的值。

解構聲明不僅可以用作函數中的頂層語句,還可以用在其他可以聲明變量的地方,例如使用in循環來枚舉map中的條目:

運行結果爲:

see you

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