一個減法的故事:Kotlin 擴充套件函式 ,Operator 和 效能優化

pdog發表於2018-07-06

前言

在寫自定義控制元件的時候,有時會需要對PointF物件進行一定操作,計算兩個點之間的水平間距和垂直間距。

簡化需求也就是要算出兩個點之間的差值。

用程式碼實現大概是這樣的

fun minusPoint(p1: PointF, p2: PointF): PointF {
    val dx = p1.x - p2.x
    val dy = p1.y - p2.y
    return PointF(dx, dy)
}

//使用
val p = minusPoint(p1,p2)
複製程式碼

第一次修改

這樣的寫法太Java了,因為我們用到的是Kotlin,我們可以改成這樣

fun minusPoint(p1: PointF, p2: PointF): PointF = PointF(p1.x - p2.x, p1.y - p2.y)

//使用
val p = minusPoint(p1,p2)
複製程式碼

第二次修改

當然,這樣也不夠好。我們使用Kotlin的擴充套件函式為PointF這個物件新增上一個擴充套件函式

fun PointF.minusPoint(p2: PointF): PointF = PointF(this.x - p2.x, this.y - p2.y)

//使用
val p = p1.minusPoint(p2)
複製程式碼

這樣的呼叫看起來可讀性高了非常多。

第三次修改

因為PointF自帶了offset的方法

public final void offset(float dx, float dy) {
  x += dx;
  y += dy;
}
複製程式碼

所以我們將可以改成這個樣子

fun PointF.minusPoint(p2: PointF): PointF = PointF().apply {
    this.offset(-p2.x, -p2.y)
}

//使用
val p = p1.minusPoint(p2)
複製程式碼

第四次修改

有程式設計經驗的小夥伴可能從第一次就發現了這個函式的一個“問題”,就是每次都會建立一個新的PointF物件。所以我們還可以對它進行一次“優化”

fun PointF.minusPoint(p2: PointF): PointF = this.apply {
    this.offset(-p2.x, -p2.y)
}

//使用
val p = p1.minusPoint(p2)
複製程式碼

這樣每次呼叫都不會產生新的物件,直接使用原來的物件就可以了。一切都看起來很美妙。

第五次修改

我們再次回到我們一開始的時候,我們一開始需要解決的問題是“計算兩個點的差值”,那麼從語義上來講。是不是可以簡單的描述成為這樣

val p1: Point
val p2: Point
val p = p1 - p2 
複製程式碼

瞭解Kotlin 的operator的同學可能從第一次看到需求的時候就想到了-操作符。

很明顯 ktx中就有PointF的擴充套件操作符。

/**
 * Offsets this point by the negation of the specified point and returns the result
 * as a new point.
 */
inline operator fun PointF.minus(p: PointF): PointF {
    return PointF(x, y).apply {
        offset(-p.x, -p.y)
    }
}

//使用
val p = p1 - p2
複製程式碼

再一次被Kotlin 甜到 !

第六次修改

細心的朋友發現,這個擴充套件操作符每次都返回了一個新的物件。

那是不是ktx這個函式寫的不好?

其實不是這樣的,現在回到我們的第四次的“優化”。

fun PointF.minusPoint(p2: PointF): PointF = this.apply {
    this.offset(-p2.x, -p2.y)
}

//使用
val p = p1.minusPoint(p2)
複製程式碼

現在我們來考慮一個問題,我們使用了p1物件減去p2的獲得了一個物件p ,這時p其實就是p1,而它們的屬性此時已被改變。如果這時,再去使用p1去做一些其他操作,顯然就和預期得到的結果不一樣了。

發現問題所在了嗎?我們的優化“減法”改變被減數,這顯然是不合理的。

所以我們在第五次的修改是不太合理的,但是我又不想用第六次的方案,因為它的確額外的物件,我就是餓死,死在外面,也不會吃這個語法糖的?!。

那麼應該怎麼辦呢?

上面我們說到,一個減法是不應該去改變被減數的,減法得到的值理所當然是一個新的值。

那麼是否我們就只能這樣了呢?當然不是,我們再次回到我們的需求,“獲得兩個點之間的差值”,其實這句需求還可以再增加完善一些,“獲得兩個點之間的差值,為了不產生新的物件可以直接修改其中一個點的值”

那麼到這裡可以發現,我們有一個非常合適的操作符來描述它,也就是 -=

直接來上程式碼

inline operator fun PointF.minusAssign(p: PointF) {
    this.apply {
        offset(-p.x, -p.y)
    }
}

//使用,沒有返回值
p -= p2
複製程式碼

btw,由於傳入的引數不是函式型別,這裡的inline是多餘的。

由於沒有返回值,那麼我們可以這樣呼叫

val p1 = p.apply {
    this -= center
}
複製程式碼

至此,我們的減法就算完成了。

通過這個減法,我得到了什麼?

  • 瞭解了kotlin的operator寫法
  • 瞭解了kotlin的inline的一些規則
  • 函式如果會對傳入引數進行修改,需要謹慎是否真的應該這樣做。

相關文章