Kotlin 知識梳理(8) - 運算符重載及其他約定

Kotlin 知識梳理系列文章

Kotlin 知識梳理(1) - Kotlin 基礎(chǔ)
Kotlin 知識梳理(2) - 函數(shù)的定義與調(diào)用
Kotlin 知識梳理(3) - 類、對象和接口
Kotlin 知識梳理(4) - 數(shù)據(jù)類、類委托 及 object 關(guān)鍵字
Kotlin 知識梳理(5) - lambda 表達(dá)式和成員引用
Kotlin 知識梳理(6) - Kotlin 的可空性
Kotlin 知識梳理(7) - Kotlin 的類型系統(tǒng)
Kotlin 知識梳理(8) - 運算符重載及其他約定
Kotlin 知識梳理(9) - 委托屬性
Kotlin 知識梳理(10) - 高階函數(shù):Lambda 作為形參或返回值
Kotlin 知識梳理(11) - 內(nèi)聯(lián)函數(shù)
Kotlin 知識梳理(12) - 泛型類型參數(shù)


一、本文概要

本文是對<<Kotlin in Action>>的學(xué)習(xí)筆記,如果需要運行相應(yīng)的代碼可以訪問在線環(huán)境 try.kotlinlang.org,這部分的思維導(dǎo)圖為:


Kotlin中,我們可以通過 調(diào)用自己代碼中定義的函數(shù),來實現(xiàn) 特定語言結(jié)構(gòu)。這些功能與 特定的函數(shù)命名 相關(guān),而不是與特定的類型綁定。例如,如果在你的類中定義了一個名為plus的特殊方法,那么按照約定,就可以在該類的實例上使用+運算符,這種技術(shù)稱為 約定

因為由類實現(xiàn)的接口集是固定的,而Kotlin不能為了實現(xiàn)其他接口而修改現(xiàn)有的類,因此一般 通過擴(kuò)展函數(shù)的機(jī)制 來為現(xiàn)有的類增添新的 約定方法,從而適應(yīng)任何現(xiàn)有的Java類。

二、重載算術(shù)運算符

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

2.1 重載二元運算符

假設(shè)已經(jīng)有一個數(shù)據(jù)類Point,它包含兩個成員變量,分別是x,y點的坐標(biāo)值,我們希望通過算術(shù)運算符+對兩個Point對象相加之后,能夠得到一個新的Point對象,它的成員變量x,y為原有兩個Point對象的x,y之和。


運行結(jié)果為:

在上面的代碼中,我們?yōu)?code>Point類定義了一個擴(kuò)展函數(shù)plus,這樣當(dāng)我們調(diào)用first + second,實際上執(zhí)行的是first.plus(second)方法來得到一個新的Point對象。這里需要注意的是:用于重載運算符的所有函數(shù)都需要 用 operator 關(guān)鍵字來標(biāo)記,用來表示你打算 把這個函數(shù)作為相應(yīng)的約定的實現(xiàn)。

所有可重載的二元算術(shù)運算符如下,自定義類型的運算符,基本上和標(biāo)準(zhǔn)數(shù)字類型的運算符有著相同的優(yōu)先級。

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

運算符函數(shù)和 Java

  • 當(dāng)從Java調(diào)用Kotlin運算符非常容易,只需要像普通函數(shù)一樣調(diào)用即可,例如上面的plus方法。
  • 當(dāng)從Kotlin調(diào)用Java的時候,對于與Kotlin約定匹配的函數(shù)(不要求使用operator修飾符,但是參數(shù)需要匹配名稱和數(shù)量)都可以使用運算符語言來調(diào)用。如果Java類定義了一個滿足需求的函數(shù),但是起了一個不同的名稱,可以通過定義一個擴(kuò)展函數(shù)來修正這個函數(shù)名用來替代現(xiàn)有的Java方法。

沒有用于位運算的特殊運算符

Kotlin沒有為標(biāo)準(zhǔn)數(shù)字類型Int,Long等定義任何位運算符,因此也不允許你為自定類型定義它們。相反,它使用中綴調(diào)用語法的函數(shù),可以為自定義類型定義相似的函數(shù),下面我們?yōu)?code>Point添加一個and,用于執(zhí)行位運算。


運行結(jié)果為:

這里我們不再使用operator關(guān)鍵字來聲明,而是用infix來定義一個中綴調(diào)用語法的函數(shù),其它執(zhí)行位運算的函數(shù)包括:shl、shrushr、and、or、xorinv

2.2 重載復(fù)合賦值運算符

當(dāng)在定義像plus這樣的函數(shù),Kotlin不止支持+號運算,也支持像+=這樣的 復(fù)合賦值運算符。


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


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

當(dāng)在代碼中用到+=的時候,理論上plusplusAssign都可能會被調(diào)用,如果兩個函數(shù)都有定義并且適用,那么編譯器就會報錯,例如下面這樣的定義:


編譯時的錯誤為:

解決方法有兩種:

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

Kotlin的標(biāo)準(zhǔn)庫支持集合的這兩種方法:

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

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



運行結(jié)果為:


2.3 重載一元運算符

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


運行結(jié)果為:

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

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

當(dāng)你定義incdec函數(shù)來重載自增和自減的運算符時,編譯器自動支持與普通數(shù)字類型的前綴、后綴自增運算符相同的語義。例如后綴運算會先返回變量的值,然后才執(zhí)行++操作。

三、重載比較運算符

與算術(shù)運算符一樣,在Kotlin中,可以對任何對象使用比較運算符(==!=、><),而不僅僅限于基本數(shù)據(jù)類型。

3.1 等號運算符,equals

如果在Kotlin中使用==/!=運算符,它將被轉(zhuǎn)換成equals方法的調(diào)用,和其他運算符不同的是,==!=可以用于可空運算數(shù),比較a == b會檢查a是否為飛空,如果不是就調(diào)用a.equals(b),完整的調(diào)用如下所示:

a?.equals(b) ?: (b == null)

對于data修飾的數(shù)據(jù)類,equals的實現(xiàn)將會由編譯器自動生成,如果需要手動實現(xiàn),可以參考下面的做法:

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

equals函數(shù)之所以被標(biāo)記為override,這是因為這個方法的實現(xiàn)是在Any類中定義的,而operator關(guān)鍵字在基本方法中已經(jīng)標(biāo)記了。同時,equals不能實現(xiàn)為擴(kuò)展函數(shù),因為繼承自Any類的實現(xiàn)始終優(yōu)先于擴(kuò)展函數(shù)。

3.2 排序運算符 compareTo

Kotlin中,對于實現(xiàn)了Comparable接口中定義的compareTo方法的類可以按約定調(diào)用,比較運算符<、>、<=、>=的使用將被轉(zhuǎn)換為compareTo,compareTo的返回類型必須為int,也就是說p1 < p2表達(dá)式等價于p1.compareTo(p2) < 0。

下面,我們定義一個Person類,讓其根據(jù)年齡來比較大?。?br>


運行結(jié)果為:

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

  • 如果值不同,則返回比較結(jié)果
  • 如果相同,則繼續(xù)調(diào)用下一個
  • 如果沒有更多的回調(diào)來調(diào)用,則返回0

這些回調(diào)函數(shù)可以像lambda一樣傳遞,或者像這里做的一樣,作為屬性引用傳遞。

四、集合與區(qū)間的約定

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

  • 通過下標(biāo)來獲取和設(shè)置元素,使用語法a[b],稱為 下標(biāo)運算符。
  • 檢查元素是否屬于當(dāng)前集合,使用in運算符。

4.1 通過下標(biāo)來訪問元素:get 和 set

Kotlin中,下標(biāo)運算符是一種約定,使用下標(biāo)運算符讀取元素會被轉(zhuǎn)換為get運算符方法的調(diào)用,并且寫入元素將調(diào)用set,下面我們?yōu)?code>Point類添加類似的方法:


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

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


運行結(jié)果為:

定義set函數(shù)后,就可以在賦值語句中使用下標(biāo)運算符,set的最后一個參數(shù)用來接收賦值語句中(等號)右邊的值,其他參數(shù)作為方括號內(nèi)的下標(biāo)。

4.2 in 的約定

集合支持的另一個運算符是in運算符,用于檢查某個對象是否屬于集合,相應(yīng)的函數(shù)叫做contains,下面的例子用于判斷某個點是否處于矩形范圍之內(nèi):


運行結(jié)果為:

4.3 rangeTo 的約定

要創(chuàng)建一個區(qū)間時,使用的是..語法,例如1..10代表所有從110的數(shù)字,..運算符是調(diào)用rangeTo函數(shù)的一個簡潔方法。rangeTo返回一個區(qū)間,你可以為自己的類定義這個運算符,但是,如果該類實現(xiàn)了Comparable接口,那么就不需要了,你可以通過Kotlin標(biāo)準(zhǔn)庫創(chuàng)建一個任意可比較元素的區(qū)間,這個庫定義了可以用于任何可比較元素的rangeTo函數(shù)

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

這個函數(shù)返回一個區(qū)間ClosedRanged,可以用來檢測其它一些元素是否屬于它。

作為例子,我們用LocalData來構(gòu)建一個日期的區(qū)間:


運行結(jié)果為:

上面的now..now.plusDays(10)將會被編譯器轉(zhuǎn)換為now.rangeTo(now.plusDays(10)),它并不是LocalDate的成員函數(shù),而是Comparable的一個擴(kuò)展函數(shù)。

4.4 在 "for" 循環(huán)中使用 "iterator" 的約定

for循環(huán)中使用in運算符表示 執(zhí)行迭代操作,諸如for(x in list) { }將被轉(zhuǎn)換成list.iterator()的調(diào)用,然后在上面重復(fù)調(diào)用hasNextnext方法。


運行結(jié)果為:

上面用到了 Kotlin 知識梳理(4) - 數(shù)據(jù)類、類委托 及 object 關(guān)鍵字 中介紹的通過object來實現(xiàn)匿名內(nèi)部類的知識。

五、解構(gòu)聲明和組件函數(shù)

解構(gòu)聲明的功能允許你展開單個復(fù)合值,并使用它來初始化多個單獨的變量。它再次用到了約定的原理,要在解構(gòu)聲明中初始化每個變量,將調(diào)用名為componentN的函數(shù),其中N是聲明中變量的位置。

對于數(shù)據(jù)類,編譯器為每個在主構(gòu)造方法中聲明的屬性生成一個componentN函數(shù),下面的例子顯示了如何手動為非數(shù)據(jù)類聲明這些功能:


運行結(jié)果為:

解構(gòu)聲明主要使用場景之一,是從一個函數(shù)返回多個值,這個非常有用。如果要這樣做,可以定義一個數(shù)據(jù)類來保存返回所需的值,并將它作為函數(shù)的返回類型。在調(diào)用函數(shù)之后,可以用解構(gòu)聲明的方式,來輕松的展開它,使用其中的值。

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


運行結(jié)果為:


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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 前言 人生苦多,快來 Kotlin ,快速學(xué)習(xí)Kotlin! 什么是Kotlin? Kotlin 是種靜態(tài)類型編程...
    任半生囂狂閱讀 26,276評論 9 118
  • 在前面寫了關(guān)于集合和范圍的內(nèi)容,里面包括了一點運算符重載的內(nèi)容,在這里我們來詳細(xì)了解運算符重載的知識,內(nèi)容參考《K...
    叫我旺仔閱讀 7,482評論 0 10
  • C++運算符重載-上篇 本章內(nèi)容:1. 運算符重載的概述2. 重載算術(shù)運算符3. 重載按位運算符和二元邏輯運算符4...
    Haley_2013閱讀 2,318評論 0 51
  • Kotlin 知識梳理系列文章 Kotlin 知識梳理(1) - Kotlin 基礎(chǔ)Kotlin 知識梳理(2) ...
    澤毛閱讀 2,570評論 0 4
  • 我總是習(xí)慣憧憬習(xí)慣把未來想得美好,現(xiàn)實好像并不想成全反過來給了我一記響亮的耳光。在這個算不上大學(xué)的地方,我見識了一...
    徐小雪閱讀 200評論 1 1