iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 29
0
Software Development

新手也能懂的 Kotlin Collection 賞玩門道系列 第 29

第二十九天:活用 Collection - Scope Function

前面的章節從 Collection 的基礎語法到核心程式碼都看了一輪,接下來想討論一下 Collection 可以如何活用?

我們之所以用 Collection,不止是因為它可以盛裝物件,或是它身上豐富的 API,而是因為 Collection 大多數的 method 都支援 Method Chain 的使用方式,讓各種操作之間還可以發揮綜效,也是 Kotlin 愛好者口中的「串串大法」。

比方說「a quick brown fox jump over a lazy dog」這句話是一個字串,我們可以先以空白做 split(),接著馬上把回傳 List 裡小於 4 個字元的單字濾掉,然後把所有單字的字首變大寫,最後依照字母順序排序,取得 [Brown, Quick] 的結果。

"a quick brown fox jump over a lazy dog"
    .split(" ")
    .filter { it.length > 4 }
    .map {
        it.capitalize()
    }
    .sorted() // [Brown, Quick]

從上面這段範例應該可以看到 Collection 的迷人之處,每一個步驟的操作都很簡單易懂,但串在一起就是一個可以取代很多次 for 迴圈的複雜動作。

Scope Function

上一章討論 Extension 時,有提到一個 let() 函式,它是 Kotlin 標準函式庫裡被稱為 Scope Function 的函式,除了它以外,還有 runwithapplyalsotakeIf。這些函式很常與 Collection 一併使用,讓串串大法可以更好申連、程式碼更簡潔易讀。以下就逐一介紹這 6 個函式:

let() 函式

let() 能使某個變數作用於其 Lambda,並回傳 Lambda 的結果值。我們思考以下這個情境,我們在 Collection 操作結束後想要把結果印出來,得先把結果放在一個變數後再印出來。

val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList) // [5, 4, 4]

但有了 let() 函式後,可以省掉這個暫存變數,用串串大法完成。

val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let { 
    println(it) // [5, 4, 4]
    // 還可以呼叫其他 function
} 

以上兩段程式碼的結果是完全相同的,但使用 let() 的語法更簡潔,甚至我們還可以在 let() 裡面呼叫更多 function,程式的彈性更高了。

run() 函式

run() 可以直接在物件身上呼叫,然後用 Lambda 處理 this,最後回傳 Lambda 的結果值。下面的程式碼我們將 numbers 這個 MutableList 拿 run() 做了 2 次的 add() 及 1 次的 count(),最後將結果存在 countEndsWithE 裡後輸出

val numbers = mutableListOf("one", "two", "three")
val countEndsWithE = numbers.run { 
    add("four")
    add("five")
    count { it.endsWith("e") }
}
println("There are $countEndsWithE elements that end with e.") // There are 3 elements that end with e.

with() 函式

run() 不同,with() 則是要先傳入一個參數,然後拿 Lambda 處理這個參數,最後回傳 Lambda 的結果值。下面這個情境,我們可以把 MutableListwith()this 套用一系列的動作,包括使用參數的屬性及方法。

val numbers = mutableListOf("one", "two", "three")
with(numbers) {
    println("'with' is called with argument $this") // 'with' is called with argument [one, two, three]
    println("It contains $size elements") // It contains 3 elements
}
val firstAndLast = with(numbers) {
    "The first element is ${first()}, the last element is ${last()}"
}
println(firstAndLast) // The first element is one, the last element is three

apply() 函式

run() 有點類似,apply() 可以直接在物件身上呼叫,然後用 Lambda 處理 this,但回傳的是物件本身。我們可以把這段程式碼唸成:在這個物件身上 apply 這些動作。也因此,範例裡的 apply() 的結果直接作用在 numberList 本身。

val numberList = mutableListOf(1.0)
numberList.apply {
    add(2.71)
    add(3.14)
    add(5.0)
}
println(numberList) // [1.0, 2.71, 3.14, 5.0]

also() 函式

also() 用起來和 let() 有點像,都是把物件傳給 Lambda,但 let() 回傳的是 Lambda 的結果,而 also() 則是回傳物件本身。

val numberList = mutableListOf<Double>()
numberList.apply {
        add(2.71)
        add(3.14)
        add(1.0)
    }
    .also { println(it) } // [2.71, 3.14, 1.0]

takeIf()takeUnless() 函式

最後要看的是 takeIf(),和以上 5 個有點不同,takeIf() 要判斷 Lambda 中提供的條件,若是 true 的話就回傳物件、若是 false 的話就回傳 null。takeIf() 想解決的是讓語法更貼英文、免除很多 if..else 的語句。

// 最簡單直覺的寫法
if (someObject != null && someObject.status) {
   doThis()
}

// 套用了空安全檢查後的版本
if (someObject?.status == true) {
   doThis()
}

// 使用 takeIf 後只要一句就可以完成
someObject?.takeIf{ it.status }?.apply{ doThis() }

takeUnless() 則是和 takeIf() 行為相同、但條件相反的版本。

表格整理及口訣

一次學了 6 個動作和行為類似但又有些微差異的函式一定很苦惱,這邊一樣用表格幫大家整理如下:

物件參考 回傳值 是 Extension 嗎?
let{} it Lambda 結果
run{} this Lambda 結果
run{} - Lambda 結果
with() this Lambda 結果
apply{} this Context 物件
also{} is Context 物件
takeIf{} it Context 物件 或 null
takeUnless{} it Context 物件 或 null

由於英文不是我們的母語,這幾個關鍵字可能會讓大家霧煞煞,這邊用 Julian Chu 的 這篇文章 裡的口訣來協助大家記憶:

Let it Run this literal, it Also Apply this

Let 用 it,Run 用 this,都是回傳 function literal 最後一行的值。Also 用 it,Apply 用 this,都是回傳 this。

花了這麼多篇幅就是要讓大家知道,Collection 加上 Scope Function 才是「串串大法」的精髓!

參考資料


上一篇
第二十八天:深入 Collection 核心 - Extension
下一篇
第三十天:活用 Collection - 用 kscript 做資料處理
系列文
新手也能懂的 Kotlin Collection 賞玩門道31

尚未有邦友留言

立即登入留言