翻譯說明: 翻譯水平有限,文章內可能會出現不許確甚至錯誤的理解,請多多包涵!歡迎批評和指正.每篇文章會結合本身的理解和例子,但願對你們學習Kotlin起到一點幫助.html
原文地址: [Kotlin Pearls 6] Extensions: The Good, The Bad and The Ugly java
原文做者: Uberto Barbiniandroid
擴展方法(還有屬性)對於Java開發者來講算是個新東西,實際上它們已經在C#中出現了很長時間,不過JVM對它們的支持首次出如今Kotlin中.面試
ps:擴展函數不難理解,可是在使用場景和規範上的說明和教程並很少.若是你對kotlin的擴展不太熟悉建議先看一下官方文檔對擴展的說明.若是你對擴展有了解,那麼這篇文章能幫助你們進一步掌握它.
算法
官方文檔-擴展編程
通過學習龐大的Kotlin代碼庫和瀏覽開源的Kotlin代碼後,我注意到在擴展的地方常常會出現一個現象就是,有些能提升代碼的可讀性可是有些卻弄得更難理解.bash
這也是一個在第一次用Kotlin作團隊開發是的熱門討論話題,因此我認爲經過個人成功和失敗經驗,在這裏基於擴展作個總結仍是有點價值的.app
若是你有不一樣的意見或者你有好的例子,請必定聯繫我.
函數
擴展的簡單介紹學習
在Kotlin中有兩種擴展類型:擴展函數和擴展屬性.
首先讓咱們瞭解一下什麼是一個擴展函數和如何在Kotlin中聲明它.
fun String.tail() = this.substring(1)
fun String.head() = this.substring(0, 1)
複製代碼
我剛寫了兩個函數,一個返回字符串的第一個字符,另外一個返回餘下的字符串.
這裏的重點是咱們函數名(好比tail
)放在了咱們但願去調用函數的類型的後面,在這個例子中是String
.
咱們寫一個測試方法大概長這樣:
@Test
fun `head and tail`() {t+
assertThat("abcde".head()).isEqualTo("a")
assertThat("abcde".tail()).isEqualTo("bcde")
}
複製代碼
很明顯,如你所料.咱們只是給String添加了兩個方法而已.咱們把代碼轉成java代碼再看看:
@NotNull
public static final String head(@NotNull String $this$head) {
String var10000 = $this$head.substring(0, 1);
return var10000;
}
複製代碼
發現head變成了靜態的方法,並有一個String類型的參數.那這就是一個語法糖?對,這就是個語法糖!
仍舊,擴展能提升可讀性也能變得更難理解.
擴展函數類型看上去像這樣:
String.() -> String
String.()
左邊的String咱們稱爲函數接收者
泛型擴展函數
看完上面的介紹後,咱們知道擴展函數能夠應用到任何一個類.那有沒有更多的應用場景呢?有!就是下面要說的泛型擴展函數.
fun <T> T.foo() : String = "foo $this"
複製代碼
一旦foo
在你的做用域,那麼你能夠用任何對象去調用這個方法,包括null.
下面是證明的例子:
assertThat(123.foo()).isEqualTo("foo 123")
val frank = User(1, "Frank")
assertThat(frank.foo()).isEqualTo("foo User(id=1, name=Frank)")
assertThat(null.foo()).isEqualTo("foo null")
複製代碼
咱們也能夠經過改變泛型參數去作擴展限制.比方說咱們只想讓某些類和它的子類調用:
fun <T: Number> T.doubleIt(): Double = this.toDouble() * 2
這裏有一點請注意,咱們把泛型限定爲Number還不是Number?(可空的Number).這樣的話null就不是一個被這個擴展函數容許的接收者類型了.
assertThat(123.doubleIt()).isEqualTo(246.0)
assertThat(123.4.doubleIt()).isEqualTo(246.8)
assertThat(123L.doubleIt()).isEqualTo(246.0)
//assertThat(null.doubleIt()).isEqualTo(null) 編譯失敗!
複製代碼
因此,若是咱們想限制上面提到的foo
函數只能是非空類型的函數接收者的話,咱們能夠像下面這樣聲明:
fun <T: Any> T.foo() = "foo $this"
擴展函數在中綴表示法中的應用
若是對中綴比較陌生能夠先看一下官方文檔的解釋 官方文檔-中綴表示法
舉個例子,我不太喜歡Kotlin中可空字符串的拼接.我覺得的結果應該是這樣null + null == null
和null + "A" == "A"
可是實際在Kotlin中結果是"nullnull""和"nullA".
因此咱們能夠經過中綴函數+擴展函數++ (在反引號中)來實現咱們預期的結果:
infix fun String?.`++`(s:String?):String? =
if (this == null) s else if (s == null) this else this + s
複製代碼
經過驗證,達到了語氣效果:
assertThat(null `++` null).isNull()
assertThat("A" `++` null).isEqualTo("A")
assertThat(null `++` "B").isEqualTo("B")
assertThat("A" `++` "B").isEqualTo("AB")
複製代碼
中綴表示法對開發內部使用的DSL來講很是有用.
擴展屬性
如今讓咱們看一下擴展屬性是作什麼的.
很簡單!擴展函數讓咱們能夠給現有的類添加方法,那麼擴展屬性就讓咱們能夠給現有的類添加屬性.
可能大家已經知道,Kotlin編譯器會在咱們訪問Java Bean對象的時候生成對應屬性.
public class JavaPerson {
private int age;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
複製代碼
當咱們在Kotlin中使用這個bean對象的時候,全部getters和setters都變成了屬性:
val p = JavaPerson()
p.name = "Fred"
p.age = 32
assertThat(p.name).isEqualTo("Fred")
assertThat(p.age).isEqualTo(32)
複製代碼
經過轉碼能夠看到,這些屬性都是直接經過映射到getters和setters獲得的:
L1
LINENUMBER 15 L1
ALOAD 1
LDC "Fred"
INVOKEVIRTUAL com/ubertob/extensions/JavaPerson.setName (Ljava/lang/String;)V
複製代碼
如何聲明新的屬性呢?咱們給Java的Date類添加一個millis
屬性:
var Date.millis: Long
get() = this.getTime()
set(x) = this.setTime(x)
複製代碼
測試代碼
val d = Date()
d.millis = 1001
assertThat(d.millis ).isEqualTo(1001L)
assertThat(d.millis ).isEqualTo(d.time)
複製代碼
擴展的應用
如今讓我同個一個例子來展現如何擴展優化你的代碼.FizzBuzz是一個面試會被常常問道的問題.若是你不太瞭解什麼是FizzBuzz的話能夠簡單理解成一個簡單算法題(寫一個程序打印1到100這些數字。可是遇到數字爲3的倍數的時候,打印「Fizz」替代數字,5的倍數用「Buzz」代替,既是3的倍數又是5的倍數打印「FizzBuzz」)
如今首先咱們能夠經過屬性找出打印Fizz和Buzz的數:
val Int.isFizz: Boolean
get() = this % 3 == 0
val Int.isBuzz: Boolean
get() = this % 5 == 0
複製代碼
再用咱們上面定義的可空拼接String的擴展方法++
,咱們能夠用一行代碼實現FizzBuzz:
fun Int.fizzBuzz(): String = "Fizz".takeIf { isFizz } `++` "Buzz".takeIf { isBuzz } ?: toString()
複製代碼
簡單分析一下:Int.fizzBuzz()
Int類的擴展函數,函數接收者是一個Int;"Fizz".takeIf { isFizz }
一個Int類型調用isFizz方法若是返回true就返回字符串"Fizz",不然null;"Buzz".takeIf { isBuzz }
一個Int類型調用isBuzz方法若是返回ture就返回字符串"Buzz",反則null;++
拼接左右的結果;?:toString()
若是是null就調用toString.
下面是測試代碼:
val res = (1..15).map { it.fizzBuzz()}.joinToString()
val expected = "1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz"
assertThat ( res ).isEqualTo(expected)
複製代碼
小結
何時應該用擴展何時不該該用擴展?
下面是個人我的見解.這個總結徹底是我我的的意見,你可能有不一樣的見解和理解.不過這些總結也是我經過review其餘人的代碼總結出來的,因此應該仍是有必定價值的.
據我所知,目前還可有官方的規範去說明何時應該用擴展不過我參照了kotlin標準庫總結的下面的使用模式:
isFizz
)僅僅爲了在DSL中去掉方法的括號:
例如轉換一個Int類型成爲Duration類型 5.toHours()
比5.hours
更讓人容易理解
對用到多個字段的重要方法裏面的屬性作映射
一個參數的純函數
例 String.reverse()
類型轉換
例 Map.toList(), Int.toPrice()
接口轉換
例 Map.asSequence(), User.asPerson()
鏈式編程
例 T.apply{…}, T.let{…}
兩個參數的中綴表示法
例 A to B, HttpRoute bind {}
非侵入式的給現有類來點語法糖
例 User.fullName()
規避泛型的協變和逆變問題
例 Iterable.flatMap {…}
class MyContainerClass<in T> {
fun <U> map(f: (T) -> U): MyContainerClass<U>{...}
}
複製代碼
編譯器會報錯,由於T 前面被in
修飾符限定了,可是在map
方法裏面又出如今out的位置.咱們能夠經過把map方法改爲擴展函數來解決:
class MyContainerClass<in T> {
...
}
fun <U, T> MyContainerClass<T>.map(f: (T) -> U):
MyContainerClass<U>{...}
複製代碼
這裏屬於Kotlin泛型的知識若是有不太熟悉的朋友看的不太明白的話,後期我會出一章專門講泛型的文章