天天看點

Kotlin Jetpack 實戰 | 04. Kotlin 高階函數

前言

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​

  • 調用方:需要寫一堆的匿名内部類,啰嗦,繁瑣,毫無重點
Kotlin Jetpack 實戰 | 04. Kotlin 高階函數

仔細看上面的代碼,開發者關心的其實隻有一行代碼:

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表達式​

​三者之間的關系:

Kotlin Jetpack 實戰 | 04. Kotlin 高階函數

回過頭再看官方文檔提供的例子:

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 來實作的。

一個表格來總結:

Kotlin Jetpack 實戰 | 04. Kotlin 高階函數

思考題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 總結

  • 本文并未覆寫高階函數所有内容,但最關鍵,最難懂的部分已經講解清楚了。小夥伴們看完本文後再去看官方文檔會輕松不少
  • 再啰嗦一遍:看再多的教程,都不如親自寫幾行代碼