[譯]Kotlin珍品 6 -擴展:好的,壞的,醜的

翻譯說明: 翻譯水平有限,文章內可能會出現不許確甚至錯誤的理解,請多多包涵!歡迎批評和指正.每篇文章會結合本身的理解和例子,但願對你們學習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 == nullnull + "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標準庫總結的下面的使用模式:

    擴展屬性的使用模式

    好的使用場景:

    • 替換setters和getters
    • 經過映射重命名屬性的字段
    • 單一字段的簡單方法調用(例如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泛型的知識若是有不太熟悉的朋友看的不太明白的話,後期我會出一章專門講泛型的文章

很差的使用場景

  • 涉及IO線程和單例的複雜方法
    例 User.saveToDb(), 8080.startHttpServer()
  • 會改變全局狀態的方法
    例 「12:45」.setTime()
  • 多參數方法
    例 「Joe」.toUser(「Smith」, 「joe@gmail.com」)
  • 經常使用類型上擴展特殊(單一範圍)的方法 例 Date.isFredBirthday(), Double.getDiscountedPrice()
相關文章
相關標籤/搜索