iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 8
0
Mobile Development

30天,從0開始用Kotlin寫APP系列 第 8

Day 08 | Kotlin 的 Higher-Order Function - Part 2(完結)

Higher-Order Function

開始前先複習一下 Higher-Order function ,它是 first-class function,因此可以用在當作其他 function 的參數、函數返回值、當作變數使用以及支持 lambda,昨天已經簡單帶到了結構與概念,那今天要更深入的探討

Function type

Kotlin 中如果要使用 Funciton 當作 Method 的參數,那麼就需要使用到 () -> T 這類型的 Function type 去告訴 Compiler

  • 這個 Function 輸入值是什麼
  • 以及 Function 輸出值是什麼

Example

// Void function,沒有輸入值,沒有輸出值
() -> Unit

// 輸入值 Int, 輸出值 String
(Int) -> String

// 輸入值是兩個 Long,輸出值是 Location
(Long, Long) -> Location

// 輸入值是一個 Location 和 一個 function,輸出值是 Int
(Location, ((Long, Long) -> Location)) -> Int

// 輸入值是一個 Int,輸出值是另一個 function
(Int) -> ((Int)-> Unit)

假設今天要把使用者目前位置的經緯度重新 Format 後,再顯示給使用者,那麼我們就會需要使用者目前位置的經度( Double )與緯度( Double ),並回傳 Format 後的 String 給使用者,因此編寫起來就會長這樣,這樣各位讀者能了解嗎?

Example:

fun main() {
	println(coordinateFormate(23.55, 121.05))
}

val coordinateFormate: (Double, Double) -> String = { lat, lon ->
    "Coordinate: ($lat, $lon)"
}

那在 Function type 使用上需要記住幾個規則:

  1. 所有 Function type 都有一個帶括號的參數類型和一個返回值 (A, B) -> C ,而括號中的參數類型也可能是空的 () -> C,如果連返回值都不要,可以透過 () -> Unit 達到
  2. 可以搭配 Receiver type 使用 A.(B) -> C
  3. Kotlin 支援非同步與協程,因此還可以搭配 Suspend 關鍵字做使用 suspend () -> Unit
  4. 可以把 function types 用 Type aliases 的方式去表達, typealias MyHandler = (Int, String, Any) -> Unit

Code Block 替換 Function Types

在寫 lambda 表示式匿名函式( Anonymous function )時,會發現其實他們可以用來取代 Function types 表示法
以剛剛上面的例子延伸的話,那用 lambda 及匿名函式改寫會長的像這樣

Example:

// Function types 版本
val coordinateFormate: (Double, Double) -> String = { lat, lon ->
    "Coordinate: ($lat, $lon)"
}

// Lambda expression 版本
val coordinateFormate: String = { lat: Double, lon: Double ->
    "Coordinate: ($lat, $lon)"
}

// Anonymous function 版本
fun(lat: Double, lon: Double): String {
    return "Coordinate: ($lat, $lon)"
}

Lambda 表示式( Lambda Expression )

上面有簡單的示範了 Lambda expression 大概的長相,在 Kotlin 中,只要用大括號包起來的部份都可以算是 Function ,在括號中分成 parametersbody , body 是寫邏輯和處理 parameters 的區塊

Lambda 在被建立出來的時候並不會 new 一個新的實體出來,而是會把區塊內的 code 先存到 Memory 中,當其他 function 需要的時候在透過 Call Function的方式呼叫,這樣的寫法下 Function 可以當作變數來呼叫,因此彈性很大,所以很常用來當作另一個 Function 的傳入值

像是在寫 OnClickListener 時,就可以使用 Lambda 簡化程式碼,這邊給一個用正統方式和 Lambda 方式的比較範例,兩種攤開來比較就更會對 Lambda 更有感覺

Example:

// 正統版本
view.setOnClickListener(object : View.OnClickListener {
    override fun onClick(v: View?) {
        Log.d("Original", "Hello")
    }
})

// Lambda 版本
view.setOnClickListener({ v -> Log.d("Lambda", "Hello") })

另外,如果你的 Lambda function 是 Function 傳入值的最後一個參數,那麼可以把他寫在 Function 外面,這樣講會有點難懂,所以直接給個範例看看

fun showCircleProgressBar(message: String, asyncTask: () -> Unit) {
    val progressBarHolder: View = MainActivity.progressBarHolder
    GlobalScope.launch(Dispatchers.Main) {
        Log.d("showCircleProgressBar", message)
        progressBarHolder.visibility = View.VISIBLE
        withContext(Dispatchers.IO) { asyncTask() }
        progressBarHolder.visibility = View.GONE
    }
}

fun main() {
    showCircleProgressBar("Start doing async task"){
        // 做讀檔或是一些需要長時間的工作
        readFile()
        storeToDataBase()
    }
}

  1. 首先建立一個 showCircleProgressBar 的 Function, Function 的傳入值是另一個 function asyncTask
  2. asyncTask 是一個 Void Function
  3. showCircleProgressBar 中,寫的一個 GlobalScope 去處理非同步工作,並且告訴他要在 IO thread 上處理非同步的工作
  4. 並且在非同步工作開始前及結束後,會顯示和隱藏 Progress bar
  5. 在 main() 調用 showCircleProgressBar()
  6. 正常調用 Function 應該會長的像是
    showCircleProgressBar("Start doing async task", {
        readFile()
        storeToDataBase()
    })

但是 Lambda function 是傳入的最後一個參數,因此大括號可以擺到小括號外面,因此會長成上面範例的模樣,那大家可能會想說位啥要這樣寫呢?
根據我讀的文章的解釋,有一派人覺得這樣寫比較符合 FP 的模樣,在可讀性上會比較高,可是有些人對這種寫法還是無法接受,但如果你是用 Android Studio 寫上面那段 code 的話, JetBrains 也會建議你把大括號擺出去,因此以我的觀點還是會想要去符合 JetBrains 推荐的方法,會比較符合生態的習慣

內聯函式( Inline Function )

Inline function 是 Java 中並沒有的概念,他的特色是當你調用一個 Inline function 時,Compiler 不會產生 Function call ,而是將 Inline function 中的 code 嵌入到調用的地方
那把 Function 轉成 inline 的方法很簡單,只要在原本的 Function 前面加上 inline 即可

例如在寫 ViewModel 的時候,原本寫法會是這樣,這樣的壞處是如果 ViewModel 要被使用到很多次,那麼舊會出現一堆這種東西
Example:

val mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)

因此可以採用 inline 來解決這個問題,我們可以先把 ViewModel 的部份抽出來變成 Extension function,然後在這個 Function 前面加上 inline 即可

Example:

inline fun <reified T: ViewModel> AppCompatActivity.getViewModel(): T {
    return ViewModelProviders.of(this).get(T::class.java)
}

當我們在 Activity 需要使用 viewmodel 時,只需要這樣寫,就可以達到上面範例的效果,是不是看起來清爽很多阿~

Example:

val mainViewModel = getViewModel<MainViewModel>()

那 inline 的好處就是解決每次使用 Higher-Order function 時都必須產生 Object 與 Closure 的 Memory 消耗和運行時間開銷
但反過來說,使用 Inline function 時,會在生成一份一樣的 code 嵌入掉用的地方,因此如果是很大的 Function 使用 Inline 的話,那麼執行性能上還是會被影響的

結語

我對 Higher-Order function 的使用上也還沒有很熟悉,因此只能依照我目前了解的部份下去寫,這次介紹也讓我重新在讀了相關的資料,感覺有更深入了解一點。那 Higher-Order function 的部份應該就先講到這樣,如果在觀念上有錯誤或是不足的部份,歡迎各位大大留言指教~

Reference


上一篇
Day 07 | Kotlin 中的擴展( Extensions )與高階函數( Higher-Order Function )- Part 1
下一篇
Day 09 | Kotlin 的物件導向程式設計(Object-oriented programming, OOP)- Part 1
系列文
30天,從0開始用Kotlin寫APP30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言