0
点赞
收藏
分享

微信扫一扫

Kotlin Jetpack 实战|06. Kotlin 扩展

前言

扩展(Extension),可以说是 Kotlin 里最有意思的特性,没有之一。

本文会系统的讲解 Kotlin ​​扩展函数​​​和​​扩展属性​​​以及比较难懂的​​扩展作用域​​​和​​扩展静态解析​​​,最后再搭配一个实战环节,将​​扩展函数​​​跟前面讲的​​高阶函数​​结合到一起。

前期准备

  • 将 Android Studio 版本升级到最新
  • 将我们的 Demo 工程 clone 到本地,用 Android Studio 打开:​​github.com/chaxiu/Kotl…​​
  • 切换到分支:​​chapter_06_extension​
  • 强烈建议各位小伙伴小伙伴跟着本文一起实战,实战才是本文的精髓

正文

1. 扩展是什么?

Kotlin 的扩展,用起来就像是:​​能给一个类新增功能​​​,这个新增的功能:可以是​​函数​​​,也可以是​​属性​​。

借助 Kotlin 扩展,我们能轻易的写出这样的代码:

// 扩展函数
"KotlinJetpackInAction".log()

// 扩展属性
val isBlank = String.isNullOrBlank

以上的代码,看起来就像是我们修改了原本 ​​String​​​ 并且往里面加了方法和属性: ​​log()​​​, ​​isNullOrBlank​​。

初次见到​​扩展​​​这个特性的时候,我真的被惊艳到了。虽然​​扩展​​不是 Kotlin 独有的特性(别的现代语言也有),但是,Kotlin 能在兼容 Java 的同时引入这样的特性,那就真的很了不起了。

2. 顶层扩展 (Top Level Extension)

顶层扩展,是最常用的扩展方式,它的定义方式也很简单,以上面的两行代码为例,我们看看它们分别应该怎么定义吧。

// BaseExt.kt
package com.boycoder.kotlinjetpackinaction

// 注意!!
// 顶层扩展不能定义在任何 Class 里,不然它就变成“类内扩展”了!

// 为 String 定义扩展函数
fun String.log() {
println(this)
}

// 为 String 定义扩展属性
val String?.isNullOrBlank: Boolean
get() = this == null || this.isBlank()

3. 顶层扩展的原理是什么?

要理解​​顶层扩展​​的实现原理,直接看字节码对应的 Java 即可,前面的文章已经讲过如何将 Kotlin 代码反编译成 Java,我们直接看结果:

public static final void log(String $this$log) {
System.out.println($this$log);
}

public static final boolean isNullOrBlank(String $this$isNullOrBlank) {
return $this$isNullOrBlank == null || StringsKt.isBlank((CharSequence)$this$isNullOrBlank);
}

​顶层扩展​​​的本质,其实就是 Java 的​​静态方法​​,这跟我们在 Java 中经常写的 Utils 类其实是一个原理。Kotlin 的顶层扩展用着感觉很神奇,但它的原理异常简单。这一切都是因为 Kotlin 编译器帮我们做了一层封装和转换。

有的人可能会嗤之以鼻的说“这不就是语法糖嘛”,但我从中看到的是 Kotlin 这种追求​​简洁​​​和​​生产力​​的设计思想。

4. 类内扩展 (Declaring extensions as members)

Package 级别的​​顶层扩展​​​理解起来很简单,​​类内扩展​​会稍微复杂些。

​类内扩展​​​(Declaring extensions as members) 在官方中文站的翻译是:​​扩展声明为成员​​​,这个翻译虽然更接近本质,但太僵硬了,因此我在这里用 ​​类内扩展​​ 指代它。

​类内扩展​​​的写法跟​​顶层扩展​​​是一模一样的,区别在于它​​在其他类的里面​​。让我们来看一个例子:

// 被扩展的类
class Host(val hostname: String) {
fun printHostname() { print(hostname) }
}

class Test(val host: Host, val port: Int) {
fun printPort() { print(port) }

// 在 Test 类内给 Host 增加了一个扩展函数
// ↓
fun Host.printConnectionString() {
printHostname() // Host.printHostname()
print(":")
printPort() // Test.printPort()
}

// 在 Test 类内给 Host 增加了一个扩展属性
// ↓
val Host.isHomeEmpty: Boolean
get() = hostname.isEmpty()

fun test() {
host.printConnectionString()
}
}

fun main() {
// 报错,Host 的类内扩展,在外面无法访问,这是与顶层扩展的不同
Host("").isHomeEmpty
Host("").printConnectionString()
}

5. 扩展小结:

  • ​顶层扩展​​ 它不能定义在类内,它的作用域是 Package 级别的,能导包就能用
  • ​类内扩展​​ 它定义在其他类内,它的作用与局限在该类内
  • ​类内扩展​​ 的优势在于,它既能访问被扩展类(Host),也能访问它所在的类(Test)
  • ​扩展​​​ 并没有实际修改被扩展的类,因此我们仍然只能访问类里的​​public​​方法和属性

6. 类内扩展的原理是什么?

我们直接看反编译后的 Java:

// Host 类并没有新增任何属性和方法
// ↓
public final class Host {
...
public final void printHostname() {
String var1 = this.hostname;
System.out.print(var1);
}
}

public final class Test {
public final void printPort() {
System.out.print(var1);
}
// Host 的扩展函数,变成了 Test 的成员函数,Host 变成了参数
// ↓ ↓
public final void printConnectionString(Host $this$printConnectionString) {
$this$printConnectionString.printHostname();
String var2 = ":";
System.out.print(var2);
this.printPort();
}
// Host 的扩展属性,也变成了 Test 的成员函数,Host 变成了参数
// ↓ ↓
public final boolean isHomeEmpty(Host $this$isHomeEmpty) {
CharSequence var2 = (CharSequence)$this$isHomeEmpty.getHostname();
return var2.length() == 0;
}
}

我们回过头来看 ​​类内扩展​​​ 的英文:(Declaring extensions as members),这非常接近它的本质。看到这里,各位应该明白这两个名字的差别:​​类内扩展​​​描述的是表象;​​扩展声明为成员​​描述的是原理。

另外,在上面这个案例中,Test 叫做​​分发接收者​​​(Dispatch Receiver),Host 叫做​​扩展接受者​​​(Extension Receiver)。这……是不是好像在哪听过类似的名字?对!这里跟上一章节:高阶函数​​带接收者的函数类型​​相呼应了。

7. 扩展函数的类型是什么?

上一章节讲​​带接收者的函数类型​​的时候,我讲过这样一句话:

从外表上看,带接收者的函数类型,就等价于成员函数(也等价于扩展函数)。但从本质上讲,它仍是通过编译器注入 this 来实现的。

一个表格来总结:

Kotlin Jetpack 实战|06. Kotlin 扩展_java

所以说,​​带接收者的函数类型​​​和​​扩展函数​​的语法设计也是一样的。

下面是我在 Demo 里写的验证代码,感兴趣的小伙伴可以去 ​​TestExt.kt​​ 实际运行一下:

fun testFunctionType() {
var lambda: A.(B, C) -> D? = A::test
lambda = A::testExt
lambda = ::testReceiver
var

8. 扩展是静态的

扩展是静态的。

这句话的潜台词是:扩展不支持​​多态​​。看这个代码案例很容易就能理解:

open class Shape

class Rectangle: Shape()

fun Shape.getName() = "Shape"

fun Rectangle.getName() = "Rectangle"

fun printClassName(s: Shape) {
println(s.getName())
}

printClassName(Rectangle())
// 输出: Shape

这个特性虽然反直觉,但是很容易理解,以后我们使用过程当中注意一下就好。

以上代码的具体细节可以看我这个 ​​GitHub Commit​​

9. 类内扩展 override,扩展函数冲突

这部分是扩展函数相对难理解的部分,文字不容易解释,只有实际运行代码通过反编译才能弄清楚,请到 Demo 工程中找到 ​​TestExtAsMember.kt​​ 运行代码,然后反编译思考一下。相关解释我已经写到注释里了。代码案例也是直接用的官方文档里的,这个例子设计的很巧妙。

​TestExtAsMember.kt​​ 的代码如下:

open class Base{ }

class Derived : Base() { }

open class BaseCaller{
open fun Base.printFunctionInfo() {
println("Base extension function in BaseCaller")
}

open fun Derived.printFunctionInfo() {
println("Derived extension function in BaseCaller")
}

val Derived.test: Int
get() = 1

fun call(b: Base) {
b.printFunctionInfo() // 调用扩展函数
}
}

class DerivedCaller: BaseCaller() {
override fun Base.printFunctionInfo() {
println("Base extension function in DerivedCaller")
}

override fun Derived.printFunctionInfo() {
println("Derived extension function in DerivedCaller")
}
}

/**
* 步骤:先运行代码,然后调试代码,最后反编译代码。
*
* 理解这个例子的关键在于:
*
* BaseCaller().call(), DerivedCaller().call() 是多态的。
*
* 而 call 函数里的 base.printFunctionInfo() 是静态的。
*
* 这段话一定要结合反编译后的代码看
*
*/
fun main()

以上代码的具体细节可以看我这个 ​​GitHub Commit​​

6. 实战

学了这么多理论,终于到我们的实战环节了。

7. 扩展函数 + SharedPreferences

还记得 Java 的 SharedPreferences 有多麻烦吗?这种模版代码我们是否写过很多?

SharedPreferences sharedPreferences= getSharedPreferences("data",Context.MODE_PRIVATE);
SharedPreferences.Editor editor =

Java 时代我们可以封装类似 PreferencesUtils 来避免模版代码。而 Kotlin 的扩展函数能让我们的代码看起来更加的简洁。接下来,我们为 SharedPreferences 增加一个扩展函数:

fun SharedPreferences.edit(
commit: Boolean = false,
action: SharedPreferences.Editor.() -> Unit {
val editor = edit()
action(editor)
if (commit) {
editor.commit()
} else

这个扩展函数很简单,我们直接看怎么用它吧。

// MainActivity.kt
private val preference: SharedPreferences by lazy(LazyThreadSafetyMode.NONE) {
getSharedPreferences(SP_NAME, MODE_PRIVATE)
}

private fun display(response: String?)

是不是清爽很多?我们终于有地方缓存 API 请求了。😂

注:另外,我们还可以结合 Kotlin 的其他特性将 SharedPreferences 封装的更加彻底,这个我们下一篇文章会讲哈。

8. 扩展函数 + Spannable

Java 里要写一个复杂的 SpannableString,是件很痛苦的事情,我随手搜一段老代码,不知能否唤起你的痛苦记忆:

SpannableString spannableString = new SpannableString("设置各种不同的字体风格:叶应是叶");
TextView tv_styleSpan = (TextView) findViewById(R.id.tv_styleSpan);

StyleSpan bold = new StyleSpan(Typeface.BOLD);
StyleSpan italic = new StyleSpan(Typeface.ITALIC);
StyleSpan boldItalic = new StyleSpan(Typeface.BOLD_ITALIC);

spannableString.setSpan(bold, 12, 13, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
spannableString.setSpan(italic, 13, 14, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
spannableString.setSpan(boldItalic, 14, 16, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

tv_styleSpan.setText(spannableString);

让我们看看借助 Kotlin 的扩展函数能做出什么样的事情吧:

这是我们接下来要实现的效果,虽然它看着是4行文字,但它却是在一个 TextView 里展示的:

Kotlin Jetpack 实战|06. Kotlin 扩展_Android_02

在 Java 里要实现这样一个效果得费不少力气,但借助 Kotlin 扩展函数,我们写一个这样的效果简直是不费吹灰之力:

// MainActivity.kt
username.text = ktxSpan {
name!!.bold().italic().size(1.3F).background(Color.YELLOW)
.append("\n")
.append("\n")
.append("Google".strike().italic().size(0.8F).color(Color.GRAY))
.append("\n")
.append(company!!.color(Color.BLUE).underline())
.append("\n")
.append("\n")
.append(url(blog!!, blog))
}

对应的 Kotlin 扩展函数是怎么实现的?其实也不难,前后不过 20 行代码:

这是入口函数,它接收一个初始值,还有一个 Lambda 表达式。注释写的很详细,我就不多解释了:

/**
* 顶层函数,作为 Span DSL 的入口类
*
* 这里用到一个重要知识点:CharSequence.() -> SpannableString 与 (CharSequence) -> SpannableString 等价
*/
fun ktxSpan(s: CharSequence = SpannableString(""), func: CharSequence.() -> SpannableString) = func(s)

这是整个 ktxSpan 的核心代码:

/**
* 核心代码 setSpan(o, 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
*/
private fun span(s: CharSequence, o: Any): SpannableString = when (s) {
is SpannableString -> s
else -> SpannableString(s)
}.apply { setSpan(o, 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) }

这里用扩展函数封装了各种 Span 的 Api:

/**
* 借助扩展函数,实现方便的 Api
*/
fun CharSequence.bold(s: CharSequence = span(s, StyleSpan(android.graphics.Typeface.BOLD))
fun CharSequence.italic(s: CharSequence = span(s, StyleSpan(android.graphics.Typeface.ITALIC))
fun CharSequence.underline(s: CharSequence = span(s, UnderlineSpan())
fun CharSequence.strike(s: CharSequence = span(s, StrikethroughSpan())
/*部分代码省略*/

各位小伙伴可以去下载 Demo 调试运行一下: ​​github.com/chaxiu/Kotl…​​,欢迎 Star Fork。

思考题1:

​这个 ktxSpan 还有优化的空间,你知道该怎么优化吗?​

思考题2:

​我们在前面高阶函数里写的 HTML DSL,是否也能用扩展来优化?​

思考题3:

​Kotlin 顶层扩展解决了 Java 的哪些问题?​

思考题4:

​Kotlin 类内扩展有哪些实际使用场景?​

9. 结尾

  • Kotlin​​顶层扩展​​解决了 Java 各种 Utils 的问题,它不仅提高了代码的可读性,还增强了易用性
  • 可读性:​​response.isNullOrBlank()​​​ 比​​TextUtils.isEmpty(response)​​ 可读性更好
  • 易用性:当我们在 IDE 里输入:​​response.​​​ IDE 就会提示我们​​response.isNullOrBlank()​​​,而 TextUtils 则无法自动提示。
举报

相关推荐

0 条评论