写点什么

Kotlin- 编程核心基石—高阶函数,androidrom 定制

用户头像
Android架构
关注
发布于: 刚刚

请问各位小伙伴有没有觉得这样的代码很啰嗦?


现在我们假装自己是语言设计者,让我们先看看上面的代码存在哪些问题:


  • 定义方:每增加一个方法,就要新增一个接口:OnClickListenerOnContextClickListener

  • 调用方:需要写一堆的匿名内部类,啰嗦,繁琐,毫无重点



仔细看上面的代码,开发者关心的其实只有一行代码:


gotoPreview();


如果将其中的核心逻辑抽出来,这样子才是最简明的:


image.setOnClickListener { gotoPreview() }image.setOnContextClickListener { gotoPreview() }


Kotlin 语言的设计者是怎么做的?是这样:


  • 用函数类型替代接口定义

  • 用 Lambda 表达式作为函数参数


与上面 View.java 的等价 Kotlin 代码如下:


//View.ktvar mOnClickListener: ((View) -> Unit)? = nullvar 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 又是个什么鬼?

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?) {gotoPreview(v)}})

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 = initialfor (element: T in this) {accumulator = combine(accumulator, element)}return accumulator}


看看这个函数类型:(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.namewebsite.text = user.blogimage.setOnClickListener { gotoImagePreviewActivity(user) }}


修改后:


user?.apply {...username.text = namewebsite.text = blogimage.setOnClickListener { gotoImagePreviewActivity(this) }}


请问:这个 apply 方法应该怎么实现?


上面的写法其实是简化后的 Lambda 表达式,让我们来反推,看看它简化前是什么样的:


// apply 肯定是个函数,所以有 (),只是被省略了 user?.apply() {...}


// Lambda 肯定是在 () 里面 user?.apply({ ... })


// 由于 gotoImagePreviewActivity(this) 里的 this 代表了 user// 所以 user 应该是 apply 函数的一个参数,而且参数名为:thisuser?.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.namewebsite.text = self.blogimage.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.namewebsite.text = blogimage.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.namewebsite.text = this.blogimage.setOnClickListener { gotoImagePreviewActivity(this) }}


现在,关键来了。上面的 apply 方法是不是看起来就像是在 User 里增加了一个成员方法 apply()?


class User() {val name: String = ""val blog: String = ""


fun apply() {// 成员方法可以通过 this 访问成员变量 username.text = this.namewebsite.text = this.blogimage.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://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>

4-2 HTML Kotlin DSL 实现

4-2-1 定义节点元素的接口

interface Element {// 每个节点都需要实现 render 方法 fun render(builder: StringBuilder, indent: String): String}


所有的 HTML 节点都要实现 Element 接口,并且在 render 方法里实现 HTML 代码的拼接:<title> Kotlin Jetpack In Action </title>

4-2-2 定义基础类

/**


  • 每个节点都有 name,content: <title> Kotlin Jetpack In Action </title>*/open class BaseElemen


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


t(val name: String, val content: String = "") : Element {// 每个节点,都会有很多子节点 val children = ArrayList<Element>()// 存放节点参数:<img src= "" alt=""/>,里面的 src,altval hashMap = HashMap<String, String>()


/**


  • 拼接 Html: <title> Kotlin Jetpack In Action </title>*/override fun render(builder: StringBuilder, indent: String): String {builder.append("name>\n")if (content.isNotBlank()) {builder.append(" content\n")}children.forEach {

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Kotlin-编程核心基石—高阶函数,androidrom定制