學習了 Kotlin 後,寫代碼時常有一種「鬧革命」的衝動,老是但願運用語法糖推翻「舊世界」。本文概括了 Kotlin 語法糖在項目實戰中的綜合運用,以實際問題爲索引,在分析解決方案的同時介紹相關語法知識。java
這是該系列的第九篇,系列文章目錄以下:bash
當構造複雜對象時,須要不少參數,若是將全部參數都經過一個構造函數來傳遞,缺少靈活性,但若是重載若干個帶有不一樣參數的構造函數,代碼就變得臃腫。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模式 有兩個優點:
50
,43
哪一個是年齡,哪一個是體重。但 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
複製代碼
調試程序時,常常須要打印列表內容,一般會這樣打印:
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()
複製代碼
有些數據類字段比較多,調試時,想把它們統統打印出來,在 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 語法糖以下:
isData
是KClass
中的一個屬性,用於判斷該類是否是一個data class
。KClass
是 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,
}
複製代碼