前言
1. 高階函數有多重要?
高階函數,在 Kotlin 裡有着舉足輕重的地位。它是 Kotlin 函數式程式設計的基石,它是各種架構的關鍵元素,比如:
協程
,
Jetpack Compose
,
Gradle Kotlin DSL
。高階函數掌握好了,會讓我們在讀源碼的時候“如虎添翼”。
本文将以盡可能簡單的方式講解
Kotlin 高階函數
,
Lambda 表達式
,以及
函數類型
。在本文的最後,我們将自己動手編寫一個
HTML Kotlin DSL
。
前期準備
- 将 Android Studio 版本更新到最新
- 将我們的 Demo 工程 clone 到本地,用 Android Studio 打開:github.com/chaxiu/Kotl…
- 切換到分支:
chapter_04_lambda
- 強烈建議各位小夥伴小夥伴跟着本文一起實戰,實戰才是本文的精髓
正文
1. 函數類型,高階函數,Lambda,它們分别是什麼?
1-1 函數類型(Function Type)是什麼?
顧名思義:函數類型,就是函數的類型。
// (Int, Int) ->Float
// ↑ ↑ ↑
fun add(a: Int, b: Int): Float { return
将函數的和
參數類型
抽象出來後,就得到了
傳回值類型
。
函數類型
就代表了
(Int, Int) -> Float
是 兩個 Int
參數類型
為 Float 的函數類型。
傳回值類型
1-2 高階函數是什麼?
高階函數是将函數用作參數或傳回值的函數。
上面的話有點繞,直接看例子吧。如果将 Android 裡點選事件的監聽用 Kotlin 來實作,它就是一個典型的
高階函數
。
// 函數作為參數的高階函數
// ↓
fun setOnClickListener(l: (View) -> Unit)
1-3 Lambda 是什麼?
Lambda 可以了解為函數的
簡寫
。
fun onClick(v: View): Unit { ... }
setOnClickListener(::onClick)
// 用 Lambda 表達式來替代函數引用
看到這,如果你沒有疑惑,那恭喜你,這說明你的悟性很高,或者說你基礎很好;如果你感覺有點懵,那也很正常,請看後面詳細的解釋。
2. 為什麼要引入 Lambda 和 高階函數?
剛接觸到高階函數和 Lambda 的時候,我就一直有個疑問:為什麼要引入 Lambda 和 高階函數?這個問題,官方文檔裡沒有解答,是以我隻能自己去尋找。
2-1 Lambda 和 高階函數解決了什麼問題?
這個問題站在語言的設計者角度會更明了,讓我們看個實際的例子,這是 Android 中的 View 定義,我省略了大部分代碼:
// View.java
private OnClickListener mOnClickListener;
private OnContextClickListener mOnContextClickListener;
// 監聽手指點選事件
public void setOnClickListener(OnClickListener l) {
mOnClickListener = l;
}
// 為傳遞這個點選事件,專門定義了一個接口
public interface OnClickListener {
void onClick(View v);
}
// 監聽滑鼠點選事件
public void setOnContextClickListener(OnContextClickListener l) {
getListenerInfo().mOnContextClickListener = l;
}
// 為傳遞這個滑鼠點選事件,專門定義了一個接口
public interface OnContextClickListener {
boolean onContextClick(View v);
}
Android 中設定點選事件和滑鼠點選事件,分别是這樣寫的:
// 設定手指點選事件
image.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
gotoPreview();
}
});
// 設定滑鼠點選事件
image.setOnContextClickListener(new View.OnContextClickListener() {
@Override
public void onContextClick(View v)
請問各位小夥伴有沒有覺得這樣的代碼很啰嗦?
現在我們假裝自己是語言設計者,讓我們先看看上面的代碼存在哪些問題:
- 定義方:每增加一個方法,就要新增一個接口:
,OnClickListener
OnContextClickListener
- 調用方:需要寫一堆的匿名内部類,啰嗦,繁瑣,毫無重點
仔細看上面的代碼,開發者關心的其實隻有一行代碼:
gotoPreview();
如果将其中的核心邏輯抽出來,這樣子才是最簡明的:
image.setOnClickListener { gotoPreview() }
image.setOnContextClickListener { gotoPreview() }
Kotlin 語言的設計者是怎麼做的?是這樣:
- 用函數類型替代接口定義
- 用 Lambda 表達式作為函數參數
與上面 View.java 的等價 Kotlin 代碼如下:
//View.kt
var mOnClickListener: ((View) -> Unit)? = null
var mOnContextClickListener: ((View) -> Unit)? = null
fun setOnClickListener(l: (View) -> Unit) {
mOnClickListener = l;
}
fun setOnContextClickListener(l: (View) -> Unit)
以上做法有以下的好處:
- 定義方:減少了兩個接口類的定義
- 調用方:代碼更加簡明
細心的小夥伴可能已經發現了一個問題:Android 并沒有提供 View.java 的 Kotlin 實作,為什麼我們 Demo 裡面可以用 Lambda 來簡化事件監聽?
// 在實際開發中,我們經常使用這種簡化方式
原因是這樣的:由于
OnClickListener
符合
SAM 轉換
的要求,是以編譯器自動幫我們做了一層轉換,讓我們可以用 Lambda 表達式來簡化我們的函數調用。
那麼,
SAM
又是個什麼鬼?
2-2 SAM 轉換(Single Abstract Method Conversions)
SAM
(Single Abstract Method),顧名思義,就是:隻有一個抽象方法的類或者接口,但在 Kotlin 和 Java8 裡,SAM 代表着:隻有一個抽象方法的接口。符合 SAM 要求的接口,編譯器就能進行 SAM 轉換:讓我們可以用 Lambda 表達式來簡寫接口類的參數。
注:Java8 中的 SAM 有明确的名稱叫做:
函數式接口
(FunctionalInterface)。
FunctionalInterface 的限制如下,缺一不可:
- 必須是接口,抽象類不行
- 該接口有且僅有一個抽象的方法,抽象方法個數必須是1,預設實作的方法可以有多個。
也就是說,對于 View.java 來說,它雖然是 Java 代碼,但 Kotlin 編譯器知道它的參數
OnClickListener
符合 SAM 轉換的條件,是以會自動做以下轉換:
轉換前:
public void setOnClickListener(OnClickListener l)
轉換後:
fun setOnClickListener(l: (View) -> Unit)
// 實際上是這樣:
fun setOnClickListener(l: ((View!) -> Unit)?)
((View!) -> Unit)?
代表,這個參數可能為空。
2-3 Lambda 表達式引發的8種寫法
當 Lambda 表達式作為函數參數的時候,有些情形下是可以簡寫的,這時候可以讓我們的代碼看起來更簡潔。然而,大部分初學者對此也比較頭疼,同樣的代碼,能有 8 種不同的寫法,确實也挺懵的。
要了解 Lambda 表達式的簡寫邏輯,其實很簡單,那就是:
多寫
。
各位小夥伴可以跟着我接下來的流程來一起寫一寫:
2-3-1 第1種寫法
這是原始代碼,它的本質是用 object 關鍵字定義了一個
匿名内部類
:
image.setOnClickListener(object: View.OnClickListener {
override fun onClick(v: View?)
2-3-2 第2種寫法
如果我們删掉
object
關鍵字,它就是 Lambda 表達式了,是以它裡面 override 的方法也要跟着删掉:
image.setOnClickListener(View.OnClickListener { view: View? ->
gotoPreview(view)
})
上面的
View.OnClickListener
被稱為:
SAM Constructor
—— SAM 構造器,它是編譯器為我們生成的。Kotlin 允許我們通過這種方式來定義 Lambda 表達式。
思考題:
這時候,
View.OnClickListener {}
在語義上是 Lambda 表達式,但在文法層面還是
匿名内部類
。這句話對不對?
2-3-3 第3種寫法
由于 Kotlin 的 Lambda 表達式是不需要
SAM Constructor
的,是以它也可以被删掉。
image.setOnClickListener({ view: View? ->
gotoPreview(view)
})
2-3-4 第4種寫法
由于 Kotlin 支援
類型推導
,是以
View?
可以被删掉:
image.setOnClickListener({ view ->
gotoPreview(view)
})
2-3-5 第5種寫法
當 Kotlin Lambda 表達式隻有一個參數的時候,它可以被寫成
it
。
image.setOnClickListener({ it ->
gotoPreview(it)
})
2-3-6 第6種寫法
Kotlin Lambda 的
it
是可以被省略的:
image.setOnClickListener({
gotoPreview(it)
})
2-3-7 第7種寫法
當 Kotlin Lambda 作為函數的最後一個參數時,Lambda 可以被挪到外面:
image.setOnClickListener() {
gotoPreview(it)
}
2-3-8 第8種寫法
當 Kotlin 隻有一個 Lambda 作為函數參數時,
()
可以被省略:
image.setOnClickListener {
gotoPreview(it)
}
按照這個流程,在 IDE 裡多寫幾遍,你自然就會了解了。一定要寫,看文章是記不住的。
2-4 函數類型,高階函數,Lambda表達式三者之間的關系
- 将函數的
和參數類型
抽象出來後,就得到了傳回值類型
。函數類型
就代表了(View) -> Unit
是 View參數類型
為 Unit 的函數類型。傳回值類型
- 如果一個函數的參數
傳回值的類型是函數類型,那這個函數就是或者
。很明顯,我們剛剛就寫了一個高階函數,隻是它比較簡單而已。高階函數
- Lambda 就是函數的一種
簡寫
一張圖看懂:
函數類型
,
高階函數
,
Lambda表達式
三者之間的關系:
回過頭再看官方文檔提供的例子:
fun <T, R> Collection<T>.fold(
initial: R,
combine: (acc: R, nextElement: T) -> R: R {
var accumulator: R = initial
for (element: T in this) {
accumulator = combine(accumulator, element)
}
return
看看這個函數類型:
(acc: R, nextElement: T) -> R
,是不是瞬間就懂了呢?這個函數接收兩個參數,第一個參數類型是
R
,第二個參數是
T
,函數的傳回類型是
R
。
3. 帶接收者(Receiver)的函數類型:A.(B,C) -> D
說實話,這個名字也對初學者不太友好:
帶接收者的函數類型
(Function Types With Receiver),這裡面的每一個字(單詞)我都認識,但單憑這麼點資訊,初學者真的很難了解它的本質。
還是繞不開一個問題:為什麼?
3-1 為什麼要引入:帶接收者的函數類型?
我們在上一章節中提到過,用 apply 來簡化邏輯,我們是這樣寫的:
修改前:
if (user != null) {
...
username.text = user.name
website.text = user.blog
image.setOnClickListener { gotoImagePreviewActivity(user) }
}
修改後:
user?.apply {
...
username.text = name
website.text = blog
image.setOnClickListener { gotoImagePreviewActivity(this) }
}
請問:這個 apply 方法應該怎麼實作?
上面的寫法其實是簡化後的 Lambda 表達式,讓我們來反推,看看它簡化前是什麼樣的:
// apply 肯定是個函數,是以有 (),隻是被省略了
user?.apply() {
...
}
// Lambda 肯定是在 () 裡面
user?.apply({ ... })
// 由于 gotoImagePreviewActivity(this) 裡的 this 代表了 user
// 是以 user 應該是 apply 函數的一個參數,而且參數名為:this
user?.apply({ this: User -> ... })
是以,現在問題非常明确了,apply 其實接收一個 Lambda 表達式:
{ this: User -> ... }
。讓我們嘗試來實作這個 apply 方法:
fun User.apply(block: (self: User) -> Unit): User{
block(self)
return this
}
user?.apply { self: User ->
...
username.text = self.name
website.text = self.blog
image.setOnClickListener { gotoImagePreviewActivity(this) }
}
由于 Kotlin 裡面的函數形參是不允許被命名為
this
的,是以我這裡用的
self
,我們自己寫出來的 apply 仍然還要通過
self.name
這樣的方式來通路成員變量,但 Kotlin 的語言設計者能做到這樣:
// 改為 this
// ↓
fun User.apply(block: (this: User) -> Unit): User{
// 這裡還要傳參數
// ↓
block(this)
return this
}
user?.apply { this: User ->
...
// this 可以省略
// ↓
username.text = this.name
website.text = blog
image.setOnClickListener { gotoImagePreviewActivity(this) }
}
從上面的例子能看到,我們反推的 apply 實作比較繁瑣,需要我們自己調用:
block(this)
,是以 Kotlin 引入了
帶接收者的函數類型
,可以簡化 apply 的定義:
// 帶接收者的函數類型
// ↓
fun User.apply(block: User.() -> Unit): User{
// 不用再傳this
// ↓
block()
return this
}
user?.apply { this: User ->
...
username.text = this.name
website.text = this.blog
image.setOnClickListener { gotoImagePreviewActivity(this) }
}
現在,關鍵來了。上面的 apply 方法是不是看起來就像是在 User 裡增加了一個成員方法 apply()?
class User() {
val name: String = ""
val blog: String = ""
fun apply() {
// 成員方法可以通過 this 通路成員變量
username.text = this.name
website.text = this.blog
image.setOnClickListener { gotoImagePreviewActivity(this) }
}
}
是以,從外表上看,帶接收者的函數類型,就等價于成員方法。但從本質上講,它仍是通過編譯器注入 this 來實作的。
一個表格來總結:
思考題2:
帶接收者的函數類型,是否也能代表擴充函數?
思考題3:
請問:
A.(B,C) -> D
代表了一個什麼樣的函數?
4. HTML Kotlin DSL 實戰
官方文檔在高階函數的章節裡提到了:用高階函數來實作 類型安全的 HTML 建構器。官方文檔的例子比較複雜,讓我們來寫一個簡化版的練練手吧。
4-1 效果展示:
val htmlContent = html {
head {
title { "Kotlin Jetpack In Action" }
}
body {
h1 { "Kotlin Jetpack In Action"}
p { "-----------------------------------------" }
p { "A super-simple project demonstrating how to use Kotlin and Jetpack step by step." }
p { "-----------------------------------------" }
p { "I made this project as simple as possible," +
" so that we can focus on how to use Kotlin and Jetpack" +
" rather than understanding business logic." }
p {"We will rewrite it from \"Java + MVC\" to" +
" \"Kotlin + Coroutines + Jetpack + Clean MVVM\"," +
" line by line, commit by commit."}
p { "-----------------------------------------" }
p { "ScreenShot:" }
img(src = "https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/15/172b55ce7bf25419~tplv-t2oaga2asx-image.image",
alt = "Kotlin Jetpack In Action")
}
}.toString()
println(htmlContent)
以上代碼輸出的内容是這樣的:
<html>
<head>
<title>
Kotlin Jetpack In Action
</title>
</head>
<body>
<h1>
Kotlin Jetpack In Action
</h1>
<p>
-----------------------------------------
</p>
<p>
A super-simple project demonstrating how to use Kotlin and Jetpack step by step.
</p>
<p>
-----------------------------------------
</p>
<p>
I made this project as simple as possible, so that we can focus on how to use Kotlin and Jetpack rather than understanding business logic.
</p>
<p>
We will rewrite it from "Java + MVC" to "Kotlin + Coroutines + Jetpack + Clean MVVM", line by line, commit by commit.
</p>
<p>
-----------------------------------------
</p>
<p>
ScreenShot:
</p>
<img src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/15/172b55ce7bf25419~tplv-t2oaga2asx-image.image" alt="Kotlin Jetpack In Action" /img>
</body>
</html>
4-2 HTML Kotlin DSL 實作
4-2-1 定義節點元素的接口
interface Element{
// 每個節點都需要實作 render 方法
fun render(builder: StringBuilder, indent: String): String
}
4-2-2 定義基礎類
/**
* 每個節點都有 name,content: <title> Kotlin Jetpack In Action </title>
*/
open class BaseElement(val name: String, val content: String = "") : Element {
// 每個節點,都會有很多子節點
val children = ArrayList<Element>()
// 存放節點參數:<img src= "" alt=""/>,裡面的 src,alt
val hashMap = HashMap<String, String>()
/**
* 拼接 Html: <title> Kotlin Jetpack In Action </title>
*/
override fun render(builder: StringBuilder, indent: String): String {
builder.append("$indent<$name>\n")
if (content.isNotBlank()) {
builder.append(" $indent$content\n")
}
children.forEach {
it.render(builder, "$indent)
}
builder.append("$indent</$name>\n")
return
4-2-3 定義各個子節點:
// 這是 HTML 最外層的标簽: <html>
class HTML : BaseElement("html") {
fun head(block: Head.() -> Unit): Head {
val head = Head()
head.block()
this.children += head
return head
}
fun body(block: Body.() -> Unit): Body {
val body = Body()
body.block()
this.children += body
return body
}
}
// 接着是 <head> 标簽
class Head : BaseElement("head") {
fun title(block: () -> String): Title {
val content = block()
val title = Title(content)
this.children += title
return title
}
}
// 這是 Head 裡面的 title 标簽 <title>
class Title(content: String) : BaseElement("title", content)
// 然後是 <body> 标簽
class Body : BaseElement("body") {
fun h1(block: () -> String): H1 {
val content = block()
val h1 = H1(content)
this.children += h1
return h1
}
fun p(block: () -> String): P {
val content = block()
val p = P(content)
this.children += p
return p
}
fun img(src: String, alt: String): IMG {
val img = IMG().apply {
this.src = src
this.alt = alt
}
this.children += img
return img
}
}
// 剩下的都是 body 裡面的标簽
class P(content: String) : BaseElement("p", content)
class H1(content: String) : BaseElement("h1", content)
class IMG : BaseElement("img") {
var src: String
get() = hashMap["src"]!!
set(value) {
hashMap["src"] = value
}
var alt: String
get() = hashMap["alt"]!!
set(value) {
hashMap["alt"] = value
}
// 拼接 <img> 标簽
override fun render(builder: StringBuilder, indent: String): String {
builder.append("$indent<$name")
builder.append(renderAttributes())
builder.append(" /$name>\n")
return builder.toString()
}
private fun renderAttributes(): String {
val builder = StringBuilder()
for ((attr, value) in hashMap) {
builder.append(" $attr=\"$value\"")
}
return
4-2-4 定義輸出 HTML 代碼的方法
fun html(block: HTML.() -> Unit): HTML {
val html = HTML()
html.block()
return
4-3 HTML 代碼展示
class WebActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_web)
val myWebView: WebView = findViewById(R.id.webview)
myWebView.loadDataWithBaseURL(null, getHtmlStr(), "text/html", "UTF-8", null);
}
private fun getHtmlStr(): String {
return html {
head {
title { "Kotlin Jetpack In Action" }
}
body {
h1 { "Kotlin Jetpack In Action"}
p { "-----------------------------------------" }
p { "A super-simple project demonstrating how to use Kotlin and Jetpack step by step." }
p { "-----------------------------------------" }
p { "I made this project as simple as possible," +
" so that we can focus on how to use Kotlin and Jetpack" +
" rather than understanding business logic." }
p {"We will rewrite it from \"Java + MVC\" to" +
" \"Kotlin + Coroutines + Jetpack + Clean MVVM\"," +
" line by line, commit by commit."}
p { "-----------------------------------------" }
p { "ScreenShot:" }
img(src = "https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/15/172b55ce7bf25419~tplv-t2oaga2asx-image.image", alt = "Kotlin Jetpack In Action")
}
}.toString()
}
}
4-4 展示效果:
4-5 HTML DSL 的優勢
- 類型安全
- 支援自動補全
- 支援錯誤提示
- 節省代碼量,提高開發效率
4-6 小結
- 以上 DSL 代碼單純的看是很難看懂的,一定要下載下傳下來一步步調試
- 這個案例裡充斥着高階函數的運用,對了解高階函數很有幫助
- 這個案例并不完整,還有很多 HTML 特性沒有實作,感興趣的小夥伴可以完善試試
- 在這個案例裡,我盡量在克制其他 Kotlin 特性的使用,比如泛型,如果用上泛型,代碼會更簡潔,感興趣的小夥伴也可以試試,下一章我們也會講泛型,到時候再來優化
5 總結
- 本文并未覆寫高階函數所有内容,但最關鍵,最難懂的部分已經講解清楚了。小夥伴們看完本文後再去看官方文檔會輕松不少
- 再啰嗦一遍:看再多的教程,都不如親自寫幾行代碼