Kotlin知識歸納(九) —— 約定

大棋發表於2019-07-03

前序

      Java在標準庫中,有一些與特定的類相關聯的語言特性。比如,實現 java.lang.Iterable 介面的物件可以在forEach迴圈中使用。Kotlin也提供很多類似原理的特性,但是是通過呼叫特定的函式,來實現特定的語言特性,這種技術稱之為約定。(例如,實現名為plus特殊方法的類,可以在該類的物件上使用 + 運算子)

      因為類實現的介面集是固定的,Kotlin不能為了實現某個語言特性,而修改現有的Java類。但也可以通過把任意約定的方法定義為Java類的擴充套件方法,使其具備Kotlin約定的能力。

      Kotlin不允許開發者自定義自己的運算子,因為Kotlin限制了你能過載的運算子,以及運算子對應的函式名稱。

算術運算子過載

      在Java中,只有基本資料型別才可以使用算術運算子,String型別也僅侷限於使用 + 運算子,對於其他類不能使用算術運算子。

      Kotlin中使用約定最直接的例子就是算術運算子,意味著只要實現約定對應的方法,就可以對任意型別使用算數運算子。約定對應的方法都需要使用operator關鍵字修飾的,表示你將該方法作為相應的約定的實現。

二元算術運算子

運算子 函式名 表示式 轉換
*(乘法運算子) times a * b a.times(b)
/(除法運算子) div a / b a.div(b)
%(取模運算子) rem a % b a.rem(b)
+(加法運算子) plus a + b a.plus(b)
-(減法運算子) minus a - b a.minus(b)

對於自定義型別的算術運算子,與基本資料型別的算術運算子具有相同的優先順序。

      operator函式不要求兩邊運算數型別相同。但不可將兩邊運算數進行交換運算,因為Kotlin不自動支援交換性。想要支援交換性,需要在兩邊的運算型別中定義相應的算術運算子的函式。

      Kotlin不要求返回值型別必須和運算數型別相同。也允許對約定的函式進行過載,即定義多個引數型別不同operator函式。

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

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

//定義另類的operator函式
operator fun Point.plus(value: Int){
    println("x = ${x + value} y = ${y + value}")
}

fun main(args:Array<String>){
    val point1 = Point(3,4)
    val point2 = Point(3,4)
    println(point1 + point2)
    println(point1 + 1)
}
複製程式碼

運算子函式與Java

      Java中呼叫Kotlin的運算子非常簡單,只需要像普通函式一樣呼叫運算子對應的函式。但由於Java中沒有operator關鍵字,所以Java中定義約定的具體函式時,唯一的約束是需要引數的 型別 和 數量 匹配。

在Java中定義兩個加法運算子的plus方法:

#daqi.java
public class Point {
    public int x;
    public int y;

    public Point(int x ,int y){
        this.x = x;
        this.y = y;
    }

    public Point plus(Point p){
        return  new Point(x + p.x, y + p.y);
    }

    public Point plus(int p){
        return  new Point(x + p, y + p);
    }

    @Override
    public String toString() {
        return "x = " + x + " , y = " + y;
    }
}
複製程式碼

在Kotlin中為Java類宣告約定的擴充套件函式,並使用加法運算子:

#daqiKotlin.kt

//將約定的函式宣告為Java類的擴充套件函式
operator fun Point.plus(longNum:Long):Point{
    return Point(this.x + longNum.toInt(), this.y + longNum.toInt())
}

fun main(args:Array<String>){
    var point1 = Point(3,4)
    var point2 = Point(4,5)
    //使用Java定義的運算子函式
    println(point1 + point2)
    println(point1 + 1)
    println(point2 + 1L)
}


複製程式碼

Kotlin知識歸納(九) —— 約定

      擴充套件函式可以很好的對現有的Java類新增Kotlin運算子的能力,但還是要遵從擴充套件函式不能訪問privateprotected修飾的屬性或方法的特性。

複合輔助運算子

      Kotlin除了支援簡單的算術運算子過載,還支援複合賦值運算子過載,即 += 、-=等複合賦值運算子。

運算子 函式名 表示式 轉換
*= timesAssign a *= b a.timesAssign(b)
/= divAssign a /= b a.divAssign(b)
%= remAssign a %= b a.remAssign(b)
+= plusAssign a += b a.plusAssign(b)
-= minusAssign a -= b a.minusAssign(b)

      當在某型別中定義了返回該型別的基本算術運算子的operator函式,且右側運算數的型別符合該operator函式的引數的情況下,可以使用複合輔助運算子。例如,定義不同引數型別的plus函式:

operator fun Point.plus(point: Point):Point{
    x += point.x
    y += point.y
    return this
}

operator fun Point.plus(value: Int):Point{
    x += value
    y += value
    return this
}
複製程式碼

藉助plus函式使用 複合賦值運算子+= :

fun main(args: Array<String>) {
    var point1 = Point(3,4)
    var point2 = Point(4,5)
    point2 += point1
    point2 += 1
}
複製程式碼

      這意味著,使用複合輔助運算子時,基本算術運算子的方法和複合賦值運算子的方法都可能被呼叫。當存在符合兩側運算數型別的基本算術運算子的operator方法和複合賦值運算子的operator方法時,編譯器會報錯。解決辦法是:

  • 將運算子轉換為對應的operator方法,直接呼叫方法。
  • 用val替代var,使編譯器呼叫複合賦值運算子的該operator方法(例如:plusAssign)

運算子與集合

      Kotlin標準庫中支援集合的使用 + 、- 、+= 和 -= 來對元素進行增減。+ 和 - 運算子總是返回一個新的集合,+= 和 -= 運算子始終就地修改集合

一元運算子和位運算子

運算子 函式名 表示式 轉換
+ unaryPlus +a a.unaryPlus()
- unaryMinus -a a.unaryMinus()
! not !a a.not()
++ inc a++、++a a.inc()
-- dec a--、--a a.dec()

      當定義incdec函式來過載自增和自減運算子時,編譯器會自動支援與普通數字型別的字首和字尾自增運算子相同的語義。例如,呼叫字首形式 ++a,其步驟是:

  • 把 a.inc() 結果賦值給 a
  • 把 a 的新值作為表示式結果返回。

比較運算子

      與算術運算子一樣,Kotlin允許對任意型別過載比較運算子(==、!=、>、<等)。可以直接使用運算子進行比較,不用像Java呼叫equalscompareTo函式。

等號運算子

      如果在Kotlin中使用 == 運算子,它將被轉換成equals方法的呼叫。!=運算子也會被轉換為equals方法的呼叫,但結果會取反。

      與其他運算子不同,== 和 != 可以用於可空運算數,因為這些運算子會檢查運算數是否為null。null == null 總是為 true。

表示式 轉換
a == b a?.equals(b) ?: (b === null)
a != b !(a?.equals(b) ?: (b === null))

      當自定義過載equals函式時,可以參考data類自動生成的equals函式:

public boolean equals(@Nullable Object var1) {
  if (this != var1) {
     if (var1 instanceof Point) {
        Point var2 = (Point)var1;
        if (this.x == var2.x && this.y == var2.y) {
           return true;
        }
     }
     return false;
  } else {
     return true;
  }
}
複製程式碼
  • 當比較自身物件時,直接返回true。
  • 型別不同,則直接返回false。
  • 依據關鍵欄位進行判斷,條件符合就返回true。

      Kotlin提供恆等運算子(===)來檢查兩個引數是否是同一個物件的引用,與Java的==運算子相同。但===!==(同一性檢查)不可過載,因此不存在對他們的約定。

      == 運算子和 != 運算子只使用函式 equals(other: Any?): Boolean,可以覆蓋它來提供自定義的相等性檢測實現。不會呼叫任何其他同名函式(如 equals(other: Point))或 擴充套件函式,因為繼承自Any類的實現始終優先於擴充套件函式和其他同名函式

排序運算子

      在Java中,類可以實現Comparable介面,並在compareTo方法中判斷一個物件是否大於另一個物件。但只有基本資料型別可以使用 <>來比較,所有其他型別沒有簡明的語法呼叫compareTo方法,需要顯式呼叫。

      Kotlin支援相同的Comparable介面(無論是Java的還是Kotlin的Comparable介面),比較運算子將會被轉換為compareTo方法。所有在Java中實現Comparable介面的類,都可以在Kotlin中使用比較運算子。

表示式 轉換
a > b a.compareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0

      Kotlin標準庫中提供compareValuesBy函式來簡潔地實現compareTo方法。該方法接收兩個進行比較的物件,和用於比較的數值的方法引用:

data class Point(var x:Int,var y:Int):Comparable<Point>{
    override fun compareTo(other: Point): Int {
        return compareValuesBy(this,other,Point::x,Point::y)
    }
}

fun main(args: Array<String>) {
    val point1 = Point(3,4)
    var point2 = Point(4,5)

    println("result = ${point1 < point2}")
}
複製程式碼

equals方法和compareTo方法,在父類中已經新增operator,過載時無需新增。

集合與區間的約定

      處理結合最常見的是通過下標獲取和設定元素,以及檢查元素是否屬於當前集合。而這些操作在Kotlin中都提供相應的運算子語法支援:

  • 使用下標運算子a[b],獲取或設定元素。
  • 使用in運算子,檢查元素是否在集合或區間內,也可以用於迭代。

下標運算子

      使用下標運算子讀取元素會被轉換成get運算子方法的呼叫。當寫入元素時,將呼叫set

表示式 轉換
a[i] a.get(i)
a[i_1, ……, i_n] a.get(i_1, ……, i_n)
a[i] = b a.set(i, b)
a[i_1, ……, i_n] = b a.set(i_1, ……, i_n, b)

      Map也可以使用下標運算子,將鍵作為下標傳入到下標運算子中獲取對應的value。對於可變的map,同樣可以使用下標運算子修改對應鍵的value值。

注:get的引數可以是任意型別,所以當對map使用下標運算子時,引數型別時鍵的型別。

in運算子

      in運算子用於檢查某個物件是否屬於集合。它是一種約定,相應的函式為contains

表示式 轉換
a in c c.contains(a)

rangTo 約定

      當需要建立區間時,都是使用..運算子。..運算子是呼叫rangeTo函式的一種約定。

表示式 轉換
start..end start.rangeTo(end)

可以為任何類定義rangeTo函式。但是,如果該類實現了Comparable介面,那麼可以直接使用Kotlin標準庫為Comparable介面提供的rangeTo函式來建立一個區間。

public operator fun <T : Comparable<T>> T.rangeTo(that: T): ClosedRange<T> = ComparableRange(this, that)
複製程式碼

使用Java8的LocalDate來構建一個日期的區間:

fun main(args: Array<String>) {
    val now = LocalDate.now()
    val vacation = now .. now.plusDays(10)
    println(now.plusWeeks(1) in vacation)
}
複製程式碼

..運算子注意點:

  • ..運算子的優先順序低於算術運算子,但最好還是把引數括起來以避免混淆:
0 .. (n + 1)
複製程式碼
  • 區間表示式呼叫函式式Api時,必須先將區間表示式括起來,否則編譯將不通過:
(0..10).filter { 
    it % 2 == 0
}.map { 
    it * it
}.forEach { 
    println(it)
}
複製程式碼

iterator 約定

      for迴圈中可以使用in運算子來表示執行迭代。這意味著Kotlin的for迴圈將被轉換成list.iterator()的呼叫,然後反覆呼叫hasNextnext 方法。

iterator方法也是Kotlin中的一種約定,這意味iterator()可以被定義為擴充套件函式。例如:Kotlin標準庫中為Java的CharSequence定義了一個擴充套件函式iterator,使我們能遍歷一個常規的Java字串。

for(s in "daqi"){
    
}
複製程式碼

解構宣告

      Kotlin提供解構宣告,允許你展開單個複合值,並使用它來初始化多個單獨的變數。

fun main(args: Array<String>) {
    val point = Point(3,4)
    val(x,y) = point
}   
複製程式碼

      解構宣告看起來像普通的變數宣告,但他的括號中存在多個變數。但其實解構宣告也是使用了約定的原理,要在解構宣告中初始化每個變數,需要提供對應的componentN函式(其中N是宣告中變數的位置)。

val point = Point(3,4)
val x = point.component1()
val y = point.component2()
複製程式碼

Kotlin知識歸納(九) —— 約定

資料類

      Kotlin中提供一種很方便生成資料容器的方法,那就是將類宣告為資料類,也就是data類。

編譯器自動從資料類的主建構函式中宣告的所有屬性生成以下方法:

  • equals()/hashCode()
  • toString()
  • componentN() 按宣告順序對應於所有屬性
  • copy()

同時資料類必須滿足以下要求:

  • 主建構函式需要至少有一個引數(可以使用預設引數來實現無參主建構函式)
  • 主建構函式的所有引數需要標記為 val 或 var
  • 資料類不能是抽象、開放、密封或者內部的

      equals方法會檢查主建構函式中宣告的所有屬性是否相等;hashCode()會根據主建構函式中宣告的所有屬性生成一個雜湊值;componentN()會按照主建構函式中宣告的所有屬性的順序生成;toString()會按照以下格式"Point(x=3, y=4)"生成字串。

      資料類體中有顯式實現 equals()hashCode() 或者 toString(),或者這些函式在父類中有 final 實現,會使用現有函式;資料類不允許為 componentN() 以及 copy() 函式提供顯式實現。

      如果該類不是資料類,要想該類的物件也可以應用於解構宣告,需要手動宣告對應的operator修飾的componentN()函式(成員函式和擴充套件函式都可以):

fun main() {
    val(x,y) = Piont(1,2)

}

class Piont(val x:Int,val y:Int){
    operator fun component1():Int{
        return x
    }

    operator fun component2():Int{
        return y
    }
}
複製程式碼

使用場景

  • 遍歷map

      使用解構宣告快速獲取mapentry 的鍵和值,快速遍歷。

for ((key, value) in map) {
   // 直接使用該 key、value
   
}
複製程式碼
  • 從函式中返回多個變數

      建立請求儲存返回資訊的資料類,在呼叫方法獲取返回資訊時,使用解構宣告將其分成不同的值:

data class Result(val resultCode: Int, val status: Int,val body:String)
fun getHttpResult(……): Result {
    // 各種計算

    return Result(resultCode, status,josnBody)
}

------------------------------------------------------------------
//獲取返回值
val(resultCode, status,josnBody) = getHttpResult()
複製程式碼

      注意:我們也可以使用標準庫中的 Pair 類作為返回值,來實現返回兩個變數。

  • 在 lambda 表示式中解構

      和map遍歷相似,就是將lambda中的Map.Entry引數進行解構宣告:

val map = mapOf(1 to 1)
map.mapValues { (key, value) -> 
    "key = $key ,value = $value "
}
複製程式碼

注意

      由於資料類中componentN()是按照主建構函式中宣告的所有屬性的順序對應生成的。也就是說component1()返回的是主建構函式中宣告的第一個值,component2()返回的是主建構函式中宣告的第二個值,以此類推。

對於解構宣告中不需要的變數,可以用下劃線取代其名稱,Kotlin將不會呼叫相應的 componentN()

fun main(args: Array<String>) {
    val point = Point(3,4)
    val(_,y) = point
    println(y)
}   
複製程式碼

Kotlin知識歸納(九) —— 約定
      否則,你想要的值在主建構函式中宣告在第二個位置,而你不是使用下劃線取代其名稱取代第一個變數的位置時,解構宣告將使用component1()對值進行賦值,你將得不到你想要的值。

fun main(args: Array<String>) {
    val point = Point(3,4)
    //y軸座標應該是第二個位置,但由於沒有使用_佔位,將使用component1()對其進行賦值,也就是使用x軸座標對y座標進行賦值。
    val(y) = point
    println(y)
}   
複製程式碼

Kotlin知識歸納(九) —— 約定

中輟呼叫

      在提到解構宣告的地方,往往伴隨著中輟呼叫的出現。但中輟呼叫並不是什麼約定,是讓含有infix 關鍵字修飾的方法,可以像基本算術運算子一樣被呼叫。即忽略該呼叫函式的點與圓括號,將函式名放在目標物件和引數之間

//中輟呼叫
1 to "one"

//普通呼叫
1.to("one")
複製程式碼

中綴函式必須滿足以下要求:

  • 成員函式或擴充套件函式
  • 只有一個引數
  • 引數不得接受可變引數且不能有預設值

使用場景

  • 區間

使用..運算子建立的區間是一個閉區間,當我們需要建立倒序區間或者半閉區間,甚至是設定區間步長時,所使用到的downTountilstep 其實都不是關鍵字,而是一個個使用infix 關鍵字修飾的方法,只是使用中輟呼叫來進行呈現。

  • map

在建立map時,對key和vlaue使用中輟呼叫來新增元素,提高可讀性。

val map = mapOf("one" to 1,"two" to 2)
複製程式碼

中輟呼叫優先順序

      中綴函式呼叫的優先順序低於算術操作符、型別轉換以及 rangeTo 操作符。所以0 until n * 20 until (n * 2)等價。

      但中綴函式呼叫的優先順序高於布林操作符&& 與 ||、is 與 in 檢測以及其他一些操作符。所以7 in 0 until 107 in (0 until 10)等價。

參考資料:

android Kotlin系列:

Kotlin知識歸納(一) —— 基礎語法

Kotlin知識歸納(二) —— 讓函式更好呼叫

Kotlin知識歸納(三) —— 頂層成員與擴充套件

Kotlin知識歸納(四) —— 介面和類

Kotlin知識歸納(五) —— Lambda

Kotlin知識歸納(六) —— 型別系統

Kotlin知識歸納(七) —— 集合

Kotlin知識歸納(八) —— 序列

Kotlin知識歸納(九) —— 約定

Kotlin知識歸納(十) —— 委託

Kotlin知識歸納(十一) —— 高階函式

Kotlin知識歸納(十二) —— 泛型

Kotlin知識歸納(十三) —— 註解

Kotlin知識歸納(十四) —— 反射

Kotlin知識歸納(九) —— 約定

相關文章