Kotlin實戰 | 語法糖,總有一顆甜到你(持續更新)

學習了 Kotlin 後,寫代碼時常有一種「鬧革命」的衝動,老是但願運用語法糖推翻「舊世界」。本文概括了 Kotlin 語法糖在項目實戰中的綜合運用,以實際問題爲索引,在分析解決方案的同時介紹相關語法知識。java

這是該系列的第九篇,系列文章目錄以下:bash

  1. Kotlin基礎:白話文轉文言文般的Kotlin常識app

  2. Kotlin基礎:望文生義的Kotlin集合操做ide

  3. Kotlin實戰:用實戰代碼更深刻地理解預約義擴展函數函數

  4. Kotlin實戰:使用DSL構建結構化API去掉冗餘的接口方法post

  5. Kotlin基礎:屬性也能夠是抽象的學習

  6. Kotlin進階:動畫代碼太醜,用DSL動畫庫拯救,像說話同樣寫代碼喲!測試

  7. Kotlin基礎:用約定簡化相親動畫

  8. Kotlin基礎 | 2 = 12 ?泛型、類委託、重載運算符綜合應用ui

  9. Kotlin實戰 | 語法糖,總有一顆甜到你(持續更新)

棄用Builder模式

當構造複雜對象時,須要不少參數,若是將全部參數都經過一個構造函數來傳遞,缺少靈活性,但若是重載若干個帶有不一樣參數的構造函數,代碼就變得臃腫。Builder 模式能夠簡化構建過程。

在 Java 中 Builder模式 代碼以下:

public class Person {
    //'必選參數'
    private String name;
    //'如下都是可選參數'
    private int gender;
    private int age;
    private int height;
    private int weight;

    //'私有構造函數,限制必須經過構造者構建對象'
    private Person(Builder builder) {
        this.name = builder.name;
        this.gender = builder.gender;
        this.age = builder.age;
        this.height = builder.height;
        this.weight = builder.weight;
    }

    //'構造者'
    public static class Builder {
        private String name;
        private int gender;
        private int age;
        private int height;
        private int weight;

        //'必選參數必須在構造函數中傳入'
        public Builder(String name) {
            this.name = name;
        }

        //'如下是每一個非必要屬性的設值函數,它返回構造者自己用於鏈式調用'
        public Builder age(int age) {
            this.age = age;
            return this;
        }

        public Builder gender(int gender) {
            this.gender = gender;
            return this;
        }

        public Builder height(int height) {
            this.height = height;
            return this;
        }

        public Builder weight(int weight) {
            this.weight = weight;
            return this;
        }

        //'構建對象'
        public Person build() {
            return new Person(this);
        }
    }

複製代碼

而後就能夠像這樣構建Person實例:

//'使用 Builder模式'
Person p = new Person.Builder("taylor")
            .age(50)
            .gender(1)
            .weight(43)
            .build();

//'使用構造函數'
Person p2 = new Person("taylor", 50, 1, 0, 43);
複製代碼

對比之下,Builder模式 有兩個優點:

  1. 爲參數標註語義:在Builder模式中,每一個屬性的賦值都是一個函數,函數名標註了屬性語義。而直接使用構造函數時,很難分辨5043哪一個是年齡,哪一個是體重。
  2. 可選參數:Builder模式中,除了必選參數,其餘參數是可選的。但直接使用構造函數必須爲全部參數賦值,好比上例中第四個參數身高被賦值爲0。

但 Builder模式 也有代價,新增了一箇中間類Builder

使用 Kotlin 的命名參數+參數默認值+數據類語法,在沒有任何反作用的狀況下就能實現 Builder模式:

//'將Person定義爲數據類'
data class Person(
    var name: String,
    //'爲如下可選參數設置默認值'
    var gender: Int = 1,
    var age: Int= 0,
    var height: Int = 0,
    var weight: Int = 0
)

//'使用命名參數構建Person實例'
val p  = Person(name = 「taylor」,gender = 1,weight = 43)
複製代碼

關於數據類參數默認值命名參數更詳細的介紹能夠點擊這裏

若是想增長參數約束條件能夠調用require()方法:

data class Person(
    var name: String,
    var gender: Int = 1,
    var age: Int= 0,
    var height: Int = 0,
    var weight: Int = 0
){
    //'在構造函數被調用的時候執行參數合法檢查'
    init {
        require(name.isNotEmpty()){」name cant be empty「}
    }
}
複製代碼

此時若是像下面這樣構造 Person,則會拋出異常:

val p = Person(name="",gender = 1)
java.lang.IllegalArgumentException: name cant be empty
複製代碼

打印列表、map

調試程序時,常常須要打印列表內容,一般會這樣打印:

for (String str:list) {
    Log.v("test", "str="+str);
}
複製代碼

不一樣業務界面的數據類型不一樣,爲了調試,這樣的 for 循環就會散落在各處,並且列表內容會分若干條 log 輸出,中間極有可能被別的log打斷。

有沒有一個函數能夠打印包含任意數據類型的列表,並將列表內容組織成更具可讀性的字符串?

用 Kotlin 的擴展函數+泛型+高階函數就能優雅地作到:

fun <T> Collection<T>.print(map: (T) -> String) =
    StringBuilder("\n[").also { sb ->
        //'遍歷集合元素,經過 map 表達式將元素轉換成感興趣的字串,並獨佔一行'
        this.forEach { e -> sb.append("\n\t${map(e)},") }
        sb.append("\n]")
    }.toString()
複製代碼

爲集合的基類Collection新增一個擴展函數,它是一個高階函數,由於它的參數是另外一個函數,該函數用 lambda 表示。再把集合元素抽象成泛型。經過StringBuilder將全部集合內容拼接成一個自動換行的字符串。

寫段測試代碼看下效果:

data class Person(var name: String, var age: Int)

val persons = listOf(
    Person("Peter", 16),
    Person("Anna", 28),
    Person("Anna", 23),
    Person("Sonya", 39)
)

persons.print { "${it.name}_${it.age}" }.let { Log.v("test",it) }
複製代碼

打印結果以下:

V/test: [
    	Peter_16,
    	Anna_28,
    	Anna_23,
    	Sonya_39,
    ]
複製代碼

一樣地,能夠如法炮製一個打印 map 的擴展函數:

fun <K, V> Map<K, V?>.print(map: (V?) -> String): String =
    StringBuilder("\n{").also { sb ->
        this.iterator().forEach { entry ->
            sb.append("\n\t[${entry.key}] = ${map(entry.value)}")
        }
        sb.append("\n}")
    }.toString()
複製代碼

將 data 類轉換成 map

有些數據類字段比較多,調試時,想把它們統統打印出來,在 Java 中,藉助於 AndroidStudio 的 toString功能卻是能夠方便地生成可讀性很高的字串:

public class Person {
    private String name;
    private int age;

    @Override
    public String toString() {
        return 」Person{「 +
                」name=‘「 + name + ’\」 +
                」, age=「 + age +
                ‘}’;
    }
}
複製代碼

可是每新建一個數據類都要手動生成一個toString()方法也挺麻煩。

利用 Kotlin 的 data class能夠省去這一步,但打印效果是全部字段都在同一行中:

data class Person(var name: String, var age: Int)

Log.v(「test」, 「person=${Person("Peter", 16)}」)

//輸出以下:
V/test: person=Person(name=Peter, age=16)
複製代碼

若是字段不少,把它們都打印在一行中可讀性不好。

有沒有一種方法,能夠讀取一個類中全部的字段信息? 這樣咱們就能夠將他們組織成想要的形狀。請看下面這個方法:

fun Any.ofMap() =
    //'過濾掉除data class之外的其餘類'
    this::class.takeIf { it.isData }
        //'遍歷類的全部成員,過濾掉成員方法,只考慮成員屬性'
        ?.members?.filterIsInstance<KProperty<Any>>()
        //'將成員屬性名和值存儲在Pair中'
        ?.map { it.name to it.call(this) }
        //'將Pair轉換成map'
        ?.toMap()
複製代碼

爲任意 Kotlin 中的類添加一個擴展函數,它的功能是將data class中全部的字段名及其對應值存在一個 map 中。其中用到的 Kotlin 語法糖以下:

  • isDataKClass中的一個屬性,用於判斷該類是否是一個data classKClass是 Kotlin 中用來描述 類的類型KClass能夠經過對象::class語法得到。

  • members也是KClass中的一個屬性,它包含了全部類的方法和屬性。

  • filterIsInstance()Iterable接口的擴展函數,用於過濾出集合中指定的類型。

  • to是一個infix擴展函數,它的定義以下:

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
複製代碼
  • 帶有infix標識的函數只容許帶有一個參數,而且在調用時能夠省略包裹參數的括號。這種語法叫中綴表達式

寫段測試代碼,結合上一節的打印 map 函數看下效果:

data class Person(var name: String, var age: Int)

Person("Peter", 16).ofMap()?.print { it.toString() }.let { Log.v("test","$it") }
複製代碼

測試代碼先將Person實例轉換成 map,而後打印 map。輸出結果以下:

V/test:
    {
    	[age] = 16
    	[name] = Peter
    }
複製代碼

data class嵌套會發生什麼?

//'位置,嵌套在Person類中'
data class Location(var x: Int, var y: Int)
data class Person(var name: String, var age: Int, var locaton: Location? = null)

Person("Peter", 16, Location(20, 30)).ofMap()?.print { it.toString() }.let { Log.v("test", "$it") }

//'打印結果以下'
    {
    	[age] = 16
    	[locaton] = Location(x=20, y=30)
    	[name] = Peter
    }
複製代碼

指望獲得相似 Json 的打印效果,但輸出結果還差一點。是由於將Person轉化成Map時並無將嵌套的Location也轉化成鍵值對。

須要將ofMap()方法重構成遞歸調用:

fun Any.ofMap(): Map<String, Any?>? {
    return this::class.takeIf { it.isData }
        ?.members?.filterIsInstance<KProperty<Any>>()
        ?.map { member ->
            val value = member.call(this)?.let { v->
                //'若成員變量是data class,則遞歸調用ofMap(),將其轉化成鍵值對,不然直接返回值'
                if (v::class.isData) v.ofMap()
                else v
            }
            member.name to value
        }
        ?.toMap()
}
複製代碼

爲了讓打印結果也有嵌套縮進效果,打印 Map 的函數也須要相應地重構:

/**
 * 打印 Map,生成結構化鍵值對子串
 * @param space 行縮進量
 */
fun <K, V> Map<K, V?>.print(space: Int = 0): String {
    //'生成當前層次的行縮進,用space個空格表示,當前層次每一行內容都須要帶上縮進'
    val indent = StringBuilder().apply {
        repeat(space) { append(" ") }
    }.toString()
    return StringBuilder("\n${indent}{").also { sb ->
        this.iterator().forEach { entry ->
            //'若是值是 Map 類型,則遞歸調用print()生成其結構化鍵值對子串,不然返回值自己'
            val value = entry.value.let { v ->
                (v as? Map<*, *>)?.print("${indent}${entry.key} = ".length) ?: v.toString()
            }
            sb.append("\n\t${indent}[${entry.key}] = $value,")
        }
        sb.append("\n${indent}}")
    }.toString()
}
複製代碼

寫段測試代碼,看看效果:

//'座標類,嵌套在Location類中'
data class Coordinate(var x: Int, var y: Int)
//'位置類,嵌套在Person類中'
data class Location(var country: String, var city: String, var coordinate: Coordinate)
data class Person(var name: String, var age: Int, var locaton: Location? = null)

Person("Peter", 16, Location("china", "shanghai", Coordinate(10, 20))).ofMap()?.print().let { Log.v("test", "$it") }

//'打印以下'
    {
    	[age] = 16,
    	[locaton] = 
              {
    	          [city] = shanghai,
    	          [coordinate] = 
                           {
    	                       [x] = 10,
    	                       [y] = 20,
                           },
    	          [country] = china,
              },
    	[name] = Peter,
    }
複製代碼
相關文章
相關標籤/搜索