在前一篇文章中,我们知道依据程序的执行顺序分成两种执行方式,一种是同步(Synchronous) 、另一种则是非同步(Asynchronous)。
同步的执行方式会依照程序码的顺序,依序执行。如下:
fun sayHello(){
println("Hello")
println("Nice to meet you!")
}
执行 sayHello()
我们就可以得到如我们程序码顺序的结果。
Hello
Nice to Meet you!
好的,同步程序的执行方式是如此直觉:按照顺序。
假如,有一个方法 showContents()
为我们登入 FB 之後,系统会根据登入的使用者,把最近的内容列出来。
以这个例子来说,我们会有三个步骤,
fun showContents() {
val token = login(userName, password)
val contents = fetchLatestContents(token)
showContents(contents)
}
login()
会回传 Token
, fetchLatestContents()
会使用 login()
所回传的 Token
来呼叫另一个 API 取得最新的内容,最後则是把 fetchLatestContentes()
回传的结果传给 showContents()
,让它来显示内容。
其中三个函式的定义如下:
fun login(userName: String, password: String): Token{...}
fun fetchLatestContent(token: Token): List<Contents> {...}
fun showContents(contents: List<Contents>){...}
这种写法依然是我们所熟悉的同步式,但是可能会有一个问题,假如每一段函式时,都需要一段时间才能够取得资料,当我们直接呼叫,也就是使用主执行绪 (Main Thread) 来执行时,那我们就会发现画面被卡住。
例如 fetchLatestContent()
需要花费较长的时间,执行时间图如下:
这个情况是不是跟我们昨天所提到的情况很类似呢?有一个耗时的任务(I/O任务)在主执行绪上执行,那麽其他在主执行绪上的任务将无法执行,因为一次只能执行一项任务。还记得我们可以怎麽处理吗?
.
.
.
.
没错,答案就是「非同步」,那麽实务上要怎麽实作非同步的程序呢?
最直觉的想法是,由於在主执行绪上执行耗时任务会造成主执行绪卡住,所以建立新的执行绪来处理这些耗时的工作,避免占用主执行绪。
fun thread01(){
thread {
// a work in new thread
}
}
在 Kotlin 中,我们可以使用
thread{}
来新建一个执行绪,并且在这个括弧中{}
执行我们所需要执行的任务。
如果我们尝试将前面的 pseudo code 改成使用 thread{}
,会发生什麽事呢?
fun login(userName: String, password: String): Token{
thread {
// ...
return@thread token // <- 没有办法在这边直接回传值
}
}
我们会发现,在 return@thread token
这一行会出现, Type mismatch
原因在 thread{}
中,是不允许回传值的,因为 thread{}
只接受 Unit
,也就是没有回传值。
重新检视上述的情境,一个函式的结果会当作下一个函式的输入传递进去,我们要怎麽解决这个问题呢?
在 Kotlin 中,我们可以将 lambda function 当作一个参数传进函数中,所以解决这个问题的另一个思路是,将一个 lambda function 传进去函式内,当执行完成之後,就呼叫 lambda function 通知下一个函式。
将
fun login(userName: String, password: String): Token{ ... }
fun fetchLatestContent(token: Token): List<Contents> { ... }
fun showContents(contents: List<Contents>){ ... }
改成
fun loginAsync(userName: String, password: String, callback: (Token) -> Unit) {
thread {
//....
callback.invoke(token)
}
}
fun fetchLatestContentAsync(token: Token, callback: (List<Contents>) -> Unit) {
thread {
val content = service.fetchContent(token)
callback.invoke(contents)
}
}
fun showContents(contents: List<Contents>){ ... }
如果 Lambda 函式是参数的最後一项,那麽我们可以将 Lambda 函式从括弧中搬出来,让程序的结构更为清晰。
那麽我们就可以将原本的直叙式的写法,改成 Callback 的写法。如下
fun showContents(){
loginAsync(userName, password){ token ->
fetchLatestContentAsync(token){ contents ->
showContents(contents)
}
}
}
但是, 用 Callback 的写法有两个缺点,第一个是如果有多个函式都需要使用 Callback,那麽在观看程序码的时候,就会变得很不好看、不容易被维护。这就是 Callback hell (回呼地狱)。
请自行 Google Callback hell
使用 Callback 的另一个问题就是会发生控制权转移 (Inversion of Control) 的情况,什麽是控制权转移呢?看下面的范例:
假如有两个函式,它们都各有两个参数,第一个参数为输入的值,另一个参数则为一个 Lambda 函式,作为 Callback 使用。由下方的程序码可以得知,呼叫 doA
会将输入的数值利用 callback
传出去,同样的,如果呼叫 doB
也会将输入的数值利用 callback
传递出去。
fun doA(value: Int, callback: (Int) -> Unit) {
callback(value)
}
fun doB(value: Int, callback: (String) -> Unit) {
callback(value.toString())
}
假如我们将这两个函式串在一起。
doA(1){ valueA ->
doB(valueA){ valueB ->
println(valueB)
}
}
当我们呼叫上面的函式时, doA
会在它里面将输入的值透过 Lambda 函式传给 doB
。以上面的范例来说,我们最後就会列印出 1
。
如果 doA
的内部不小心呼叫两次 callback
fun doA(value: Int, callback: (Int) -> Unit) {
callback(value)
callback(value)
}
那麽原本的结果就会出现连续两个 1 。这明显不是我们想要的。
doB
将自己输入数值的控制权转交给 doA
来呼叫,当 doA
错误呼叫时,後面的呼叫就有可能出错。
那麽要怎麽避免 Callback hell、不在主执行绪上执行,又能让程序码是以非同步的方式执行呢?
能解决这个问题的方法有很多种: Futures
, Promise
, RxJava
以及本系列文章的主角 Coroutine
。
Kotlin Taiwan User Group
Kotlin 读书会
前言: 衔接完成 程序码: // // ViewController.swift // Dog Br...
昨天PO完文重看一下旧文才发现前天说要讲json做轮播,结果昨天先讲了tab…. 希望今天能讲完轮播...
💡 开始使用 Git 之前,我们需要先设定使用者名称及电子邮件地址。 为什麽需要设定用户名称及 E-...
https://bit.ly/2XuVqBJ (这篇必看,不分享对不起自己) //原来南无观世音菩...
Background 对於变数的Type,能够依据他们的特性分为两种,分别为不可变的Static t...