Android開發者快速上手Kotlin(三) 之 高階函數和SAM轉換

《Android開發者快速上手Kotlin(二) 之 面向對象編程》文章繼續。

6 高階函數

Kotlin中的高階函數其實就是跟高等數學中的高階函數一個概念,就是函數套函數,即f(g(x))。什麼意思呢?其實很好理解,就是將函數本身看作一種類型,作爲別的函數的參數傳入或者作爲返回值返回。我們在前面其實就已經接觸過高階函數,因爲:arg.forEach(::println)中,forEach本身就是一個高階函數,它接收的參數是:action: (T) -> Unit。來通過自定義是如何實現:

// 該函數返回一個函數
fun a(): (y: Int, z: Int) -> Int {
    return { a: Int, b: Int -> a + b }
}

// 該函數需要傳入第二個參數是一個函數
fun b(int: Int, function: (string:String) -> Unit) {
    function(int.toString())
}
// 調用
var aFun = a()                     // 返回一個函數
var result = aFun(2, 3)           // 執行了返回的函數
b(result, ::println)              // 輸入println函數作爲參數

說明

  1. a函數不接收參數,但返回了一個Lambda表達式: (y: Int, z: Int) -> Int。
  2. b函數接收一個Int和一個Lambda表達式:(string:String) -> Unit。
  3. 函數的引用使用兩個冒號::,如果是類對象方法,就是obj::fun()。

6.1 內聯函數 inline

當我們使用高階函數時,傳入或返回的Lambda表達式實際上它會被編譯成匿名類,那麼也意味着每調用都會創建一個新的對象,這會造成明顯的性能開銷。所以我們在使用高階函數時一般都會在前面使用關鍵字inline來進行修飾,這代碼是一個內聯函數,也就是說編譯器會在編譯時把我們實現的真實代碼替換到每一個函數調用中,而不是使用匿名類的方式。當然如果存在特殊情況,需要不內聯,也可以使用oninline關鍵字。如:

inline fun a(noinline function1: () -> Unit, function2: () -> Unit) {
    function1()
    function2()
}

6.2 常用高階函數

6.2.1 let、run、with、also和apply

let、run、with、also和apply這5個高階函數作用起基本差不多,只是在使用上有一點點區別。它們都是作用域函數,當你需要去定義一個變量在一個特定的作用域範圍內,這些函數的是一個不錯的選擇;而且它們另一個作用就是可以避免寫一些判斷null的操作。

class Persion(var name: String, var age: Int) {
    override fun toString(): String {
        return "{$name, $age}"
    }
}
// 調用
var persion1 = Persion("子云心", 30)
persion1.let {				// 返回表達式結果
    it.age = 15
    println("{${it.name}, ${it.age}}")  // 結果:{子云心, 15}
}
persion1.run {				// 直接能訪問到類對象的let版本
    println("{${name}, ${age}}")        // 結果:{子云心, 15}
}
with(persion1) {			// 非擴展函數的run版本
    println("{${name}, ${age}}")        // 結果:{子云心, 15}
}

var persion2 = persion1.also {		// 同時返回新的對象的let版本
    it.age = 18
}
println(persion2)                       // 結果:{子云心, 18}

var persion3 = persion1.apply {	// 同時返回新的對象的run版本
    age = 22
}
println(persion1)                       // 結果:{子云心, 22}
println(persion2)                       // 結果:{子云心, 22}
println(persion3)                       // 結果:{子云心, 22}

6.2.2 use自動關閉資源

user高階函數內部做了很多異常的處理和最後自動close釋放資源,所以我們在使用上不需要自己去實現異常捕獲和手動close,直接在大括號裏使用最後執行的代碼邏輯就可以了,也不怕內存泄漏。使用如:

File("build.gradle").inputStream().reader().buffered().use {
    println(it.readLines())
}

6.2.3 集合映射函數:filter、map、flatMap以及 asSequence

先來看看這三個函數的作用:

filter:     保留滿足條件的元素

map:         集合中的所有元素一一映射到其他元素構成新集合

flatMap:  集合中的所有元素一一映身到新集合併合並這些集合得到新集合

它們的使用如:

val list1: List<Int> = listOf(1, 2, 3, 4)

val list2 = list1.filter { it % 2 == 0 }
println(list2)                          // 輸出結果:[2,4]

val list3 = list1.map { it * 2 }
println(list3)                          // 輸出結果:[2, 4, 6, 8]

val list4 = list1.flatMap { 0 until it }
println(list4)                          // 輸出結果:[0, 0, 1, 0, 1, 2, 0, 1, 2, 3]

asSequence:轉換爲懶序列

在集合後加上asSequence後,集合變成爲懶序列,只有等到真正需要(調用forEach)時纔會被執行,否則它就是一個公式不會被執行。下面來對比一下加了asSequence和沒有加asSequence集合的輸出結果:

val list: List<Int> = listOf(1, 2, 3, 4)

list.asSequence()
    .filter {
        print("filter:$it,")
        it % 2 == 0
    }.map {
        print("map:$it,")
        it * 2
    }.forEach {
        print("forEach:$it,")
    }
// 輸出結果:filter:1,filter:2,map:2,forEach:4,filter:3,filter:4,map:4,forEach:8,

list.filter {
        print("filter:$it,")
        it % 2 == 0
    }.map {
        print("map:$it,")
        it * 2
    }.forEach {
        print("forEach:$it,")
    }
// 輸出結果:filter:1,filter:2,filter:3,filter:4,map:2,map:4,forEach:4,forEach:8,

6.2.4 集合的聚合函數:sum、reduce和fold

先來看看這三個函數的作用:

sum:      所有元素求和

reduce:    將元素依次按規則聚合,結果與元素類型一致

fold:          給定初始化值版本的reduce

它們的使用如:

val list1: List<Int> = listOf(1, 2, 3, 4)

val list2 = list1.sum()
println(list2)              // 輸出結果:10

val list3 = list1.reduce { acc, i -> acc + i }
println(list3)              // 輸出結果:10

val list4 = list1.fold("Hello") { acc, i -> acc + i }
println(list4)              // 輸出結果:1234

7 SAM轉換

SAM全稱是Single Abstract Method,意思是單一抽象方法。Java8中開始對Lambda和SAM轉換支持。在使用上爲:一個參數類型爲只有一個方法的接口的方法時,調用時可用Lambda表達式做轉換作爲參數。而Kotlin中的SAM轉換其實跟Java中的使用差不多,但是得加多一個限制條件,那就是隻支持Java接口的Java方法。什麼意思?其實就是Kotlin只對調用Java代碼支持SAM轉換,Kotlin調Kotlin接口方法時是不支持SAM轉換的。這是因爲語言設計者認爲,Kotlin本來就原生支持函數類型,根本沒有必要再進行單一接口方法的定義。看看看如何使用:

// Java代碼
public interface OnClickListener {
    void onClick();
}
// Kotlin調用代碼
var onClickListener1 = object : OnClickListener {       // 匿名類的調用方式
    override fun onClick() {
        println("Hello World!")
    }
}
var onClickListener2 = OnClickListener {                // SAM轉換調用方式
    println("Hello World!")
}
上述代碼,如果要將onClickListener1和onClickListener2兩個變量都傳遞給一個Java方法,可以這樣:
// Java代碼
public class ListenerManager {
    List<OnClickListener> listenerList = new ArrayList();

    public void addListener(OnClickListener listener) {
        listenerList.add(listener);
    }

    public void removeListener(OnClickListener listener) {
        listenerList.remove(listener);
    }
}
// Kotlin調用代碼
val listenerManager = ListenerManager()
listenerManager.addListener(onClickListener1)
listenerManager.addListener(onClickListener2)
補充:

ListenerManager的addListener方法因爲是Java代碼,其實我們在Kotlin中調用時,也可以使用函數類型作爲參數傳入,如:

var onClickListener3 = {                       // Kotlin中的函數類型
    println("Hello World!")
}
listenerManager.addListener(onClickListener3)

注意:

如果只是一次性調用,上述代碼是沒有問題的。但是像我們在Android開發中常用的addListener和removeListener中,這樣的調用可能就會踩到坑了。因爲函數類型在編譯時針對每次調用的地方都會new出一個新的匿名類,所以添加一時爽,移除真踩坑:

val listenerManager = ListenerManager()
listenerManager.addListener(onClickListener1)
listenerManager.addListener(onClickListener2)
listenerManager.addListener(onClickListener3)

listenerManager.removeListener(onClickListener1)        // 可以移除
listenerManager.removeListener(onClickListener2)        // 可以移除
listenerManager.removeListener(onClickListener3)        // 不可以移除,因爲跟add時不是同一個對象

 

未完,請關注後面文章更新

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章