《00. 写给 Java 开发者的 Kotlin 入坑指南》
《01. 从一个膜拜大神的 Demo 开始》
《02. 用 Kotlin 写 Gradle 脚本是一种什么体验?》
《03. Kotlin 编程的三重境界》
高阶函数,在 Kotlin 里有着举足轻重的地位。它是 Kotlin 函数式编程的基石,它是各种框架的关键元素,比如:协程
,Jetpack Compose
,Gradle Kotlin DSL
。高阶函数掌握好了,会让我们在读源码的时候“如虎添翼”。
本文将以尽可能简单的方式讲解 Kotlin 高阶函数
,Lambda 表达式
,以及函数类型
。在本文的最后,我们将自己动手编写一个 HTML Kotlin DSL
。
chapter_04_lambda
顾名思义:函数类型,就是函数的类型。
// (Int, Int) ->Float // ↑ ↑ ↑ fun add(a: Int, b: Int): Float { return (a+b).toFloat() } 复制代码
将函数的
参数类型
和返回值类型
抽象出来后,就得到了函数类型
。(Int, Int) -> Float
就代表了参数类型
是 两个 Int返回值类型
为 Float 的函数类型。
高阶函数是将函数用作参数或返回值的函数。
上面的话有点绕,直接看例子吧。如果将 Android 里点击事件的监听用 Kotlin 来实现,它就是一个典型的高阶函数
。
// 函数作为参数的高阶函数 // ↓ fun setOnClickListener(l: (View) -> Unit) { ... } 复制代码
Lambda 可以理解为函数的简写
。
fun onClick(v: View): Unit { ... } setOnClickListener(::onClick) // 用 Lambda 表达式来替代函数引用 setOnClickListener({v: View -> ...}) 复制代码
看到这,如果你没有疑惑,那恭喜你,这说明你的悟性很高,或者说你基础很好;如果你感觉有点懵,那也很正常,请看后面详细的解释。
刚接触到高阶函数和 Lambda 的时候,我就一直有个疑问:为什么要引入 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) { gotoPreview(); } }); 复制代码
请问各位小伙伴有没有觉得这样的代码很啰嗦?
现在我们假装自己是语言设计者,让我们先看看上面的代码存在哪些问题:
OnClickListener
,OnContextClickListener
仔细看上面的代码,开发者关心的其实只有一行代码:
gotoPreview(); 复制代码
如果将其中的核心逻辑抽出来,这样子才是最简明的:
image.setOnClickListener { gotoPreview() } image.setOnContextClickListener { gotoPreview() } 复制代码
Kotlin 语言的设计者是怎么做的?是这样:
与上面 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) { mOnContextClickListener = l; } 复制代码
以上做法有以下的好处:
细心的小伙伴可能已经发现了一个问题:Android 并没有提供 View.java 的 Kotlin 实现,为什么我们 Demo 里面可以用 Lambda 来简化事件监听?
// 在实际开发中,我们经常使用这种简化方式 setOnClickListener { gotoPreview() } 复制代码
原因是这样的:由于 OnClickListener
符合 SAM 转换
的要求,因此编译器自动帮我们做了一层转换,让我们可以用 Lambda 表达式来简化我们的函数调用。
那么,SAM
又是个什么鬼?
SAM
(Single Abstract Method),顾名思义,就是:只有一个抽象方法的类或者接口,但在 Kotlin 和 Java8 里,SAM 代表着:只有一个抽象方法的接口。符合 SAM 要求的接口,编译器就能进行 SAM 转换:让我们可以用 Lambda 表达式来简写接口类的参数。
注:Java8 中的 SAM 有明确的名称叫做:函数式接口
(FunctionalInterface)。
FunctionalInterface 的限制如下,缺一不可:
也就是说,对于 View.java 来说,它虽然是 Java 代码,但 Kotlin 编译器知道它的参数 OnClickListener
符合 SAM 转换的条件,所以会自动做以下转换:
转换前:
public void setOnClickListener(OnClickListener l) 复制代码
转换后:
fun setOnClickListener(l: (View) -> Unit) // 实际上是这样: fun setOnClickListener(l: ((View!) -> Unit)?) 复制代码
((View!) -> Unit)?
代表,这个参数可能为空。
当 Lambda 表达式作为函数参数的时候,有些情形下是可以简写的,这时候可以让我们的代码看起来更简洁。然而,大部分初学者对此也比较头疼,同样的代码,能有 8 种不同的写法,确实也挺懵的。
要理解 Lambda 表达式的简写逻辑,其实很简单,那就是:多写
。
各位小伙伴可以跟着我接下来的流程来一起写一写:
这是原始代码,它的本质是用 object 关键字定义了一个匿名内部类
:
image.setOnClickListener(object: View.OnClickListener { override fun onClick(v: View?) { gotoPreview(v) } }) 复制代码
如果我们删掉 object
关键字,它就是 Lambda 表达式了,因此它里面 override 的方法也要跟着删掉:
image.setOnClickListener(View.OnClickListener { view: View? -> gotoPreview(view) }) 复制代码
上面的 View.OnClickListener
被称为: SAM Constructor
—— SAM 构造器,它是编译器为我们生成的。Kotlin 允许我们通过这种方式来定义 Lambda 表达式。
这时候,View.OnClickListener {}
在语义上是 Lambda 表达式,但在语法层面还是匿名内部类
。这句话对不对?
由于 Kotlin 的 Lambda 表达式是不需要 SAM Constructor
的,所以它也可以被删掉。
image.setOnClickListener({ view: View? -> gotoPreview(view) }) 复制代码
由于 Kotlin 支持类型推导
,所以 View?
可以被删掉:
image.setOnClickListener({ view -> gotoPreview(view) }) 复制代码
当 Kotlin Lambda 表达式只有一个参数的时候,它可以被写成 it
。
image.setOnClickListener({ it -> gotoPreview(it) }) 复制代码
Kotlin Lambda 的 it
是可以被省略的。注意:虽然it
被省略了,但我们还是可以继续用它,跟 this
的感觉有点像。
image.setOnClickListener({ gotoPreview(it) }) 复制代码
当 Kotlin Lambda 作为函数的最后一个参数时,Lambda 可以被挪到外面:
image.setOnClickListener() { gotoPreview(it) } 复制代码
当 Kotlin 只有一个 Lambda 作为函数参数时,()
可以被省略:
image.setOnClickListener { gotoPreview(it) } 复制代码
按照这个流程,在 IDE 里多写几遍,你自然就会理解了。一定要写,看文章是记不住的。
参数类型
和返回值类型
抽象出来后,就得到了函数类型
。(View) -> Unit
就代表了参数类型
是 View 返回值类型
为 Unit 的函数类型。或者
返回值的类型是函数类型,那这个函数就是高阶函数
。很明显,我们刚刚就写了一个高阶函数,只是它比较简单而已。简写
一张图看懂:函数类型
,高阶函数
,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 accumulator } 复制代码
看看这个函数类型:(acc: R, nextElement: T) -> R
,是不是瞬间就懂了呢?这个函数接收两个参数,第一个参数类型是R
,第二个参数是T
,函数的返回类型是R
。当然,这里还用到了泛型,我们暂且不管,下一章节我们会讲 Kotlin 的泛型。
说实话,这个名字也对初学者不太友好:带接收者的函数类型
(Function Types With Receiver),这里面的每一个字(单词)我都认识,但单凭这么点信息,初学者真的很难理解它的本质。
还是绕不开一个问题:为什么?
我们在上一章节中提到过,用 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 应该是 Lambda 的参数,而且参数名为: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 来实现的。
一张图总结:
带接收者的函数类型,是否也能代表扩展函数?
请问:A.(B,C) -> D
代表了一个什么样的函数?
官方文档在高阶函数的章节里提到了:用高阶函数来实现 类型安全的 HTML 构建器。官方文档的例子比较复杂,让我们来写一个简化版的练练手吧。
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://user-gold-cdn.xitu.io/2020/6/15/172b55ce7bf25419?imageslim", 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://user-gold-cdn.xitu.io/2020/6/15/172b55ce7bf25419?imageslim" alt="Kotlin Jetpack In Action" /img> </body> </html> 复制代码
interface Element { // 每个节点都需要实现 render 方法 fun render(builder: StringBuilder, indent: String): String } 复制代码
所有的 HTML 节点都要实现 Element 接口,并且在 render 方法里实现 HTML 代码的拼接:<title> Kotlin Jetpack In Action </title>
/** * 每个节点都有 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 builder.toString() } } 复制代码
// 这是 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 builder.toString() } } 复制代码
fun html(block: HTML.() -> Unit): HTML { val html = HTML() html.block() return 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://user-gold-cdn.xitu.io/2020/6/15/172b55ce7bf25419?imageslim", alt = "Kotlin Jetpack In Action") } }.toString() } } 复制代码
以上修改的具体细节可以看我这个 GitHub Commit。
回目录-->《Kotlin Jetpack 实战》