本文已授权[郭霖]公众号独家发布
接触 Kotlin 的扩展函数有一段时间了,不过对这个知识的理解只是停留在顶层扩展函数而已。
在继续学习 Kotlin 的使用时,发现这样的理解是远远不够的,比如这些问题就不清楚:
本文会一一演示说明并解决这些问题,现在占用同学们几分钟时间,我们一起开始吧。
在 2.1 下,我们把顶层扩展函数简称为扩展函数。
扩展函数是定义在类的外面,这里定义一个 String
类的扩展函数,用来获取字符串的最后一个字符:
package com.kotlin.lib._1_topextensionfunction
fun String.lastChar(): Char {
return this.get(this.length - 1)
}
把要扩展的类或者接口的名称,放到即将添加的函数前面。这个类或者接口就被称为接收者类型;用来调用这个扩展函数的那个对象,叫作接收者对象。如下图所示:
使用定义好的扩展函数:
fun main() {
println("Kotlin".lastChar()) // 打印:n
}
可以看到,这个扩展函数是符合预期的。在这次调用中,String
是接收者类型,而 "Kotlin"
就是接收者对象。
从调用上看,调用lastChar()
和调用 String
类的普通成员函数的方式是一模一样的,都是通过对象.方法名的方式调用的。
另外,这里的扩展函数,可以像普通的成员函数一样,省略掉 this
:
package com.kotlin.lib._1_topextensionfunction
fun String.lastChar(): Char {
return get(length - 1)
}
在 Kotlin 中,类型和类是不一样的。
对于一个非泛型类,对应着非空类型和可空类型,如 String
类:
var x: String // 非空类型
var y: String? // 可空类型
对于一个泛型类,会存在无限数量的类型,如 List
类:
var stringList: List<String>
var nullStringList: List<String?>
var stringNullList: List<String>?
var stringListList: List<List<String>>
回到 2.1.1 中的例子,就是对 String
的非空类型添加了扩展函数。现在对 String?
定义一个扩展函数:
fun String?.firstChar(): Char? {
return this?.get(0)
}
这里需要注意的是,在 Java 中,this
永远是非空的;而在 Kotlin 中,this
是可以为空的:在可空类型的扩展函数中,this
就可以是 null
。因此,在 firstChar
内部,通过 this
来直接调用 get
方法,就会编译报错,可以使用安全调用(?.
)来解决。当然了,这种情况下,this
是不可以省略的。
调用:
fun main() {
println("Kotlin".firstChar()) // 打印:K
println(null.firstChar()) // 打印:null
}
可以对 List
类型,List
类型定义求所有元素之和的扩展函数 sum
:
package com.kotlin.lib._1_topextensionfunction
fun List<Int>.sum(): Int {
var sum: Int = 0
for (element in this) {
sum += element
}
return sum
}
fun List<Double>.sum(): Double {
var sum: Double = 0.0
for (element in this) {
sum += element
}
return sum
}
调用:
fun main() {
println(listOf(1, 2, 3).sum()) // 打印:6
println(listOf(1.1, 2.2, 3.3).sum()) // 打印:6.6
}
可以对 List
定义一个把所有元素以逗号拼接为字符串的扩展函数,这也是一个泛型扩展函数:
package com.kotlin.lib._1_topextensionfunction
fun <T> List<T>.joinToString(): String {
val result = StringBuilder()
for ((index, element) in this.withIndex()) {
if (index > 0) result.append(", ")
result.append(element)
}
return result.toString()
}
调用:
fun main() {
println(listOf(1, 2, 3).joinToString()) // 1, 2, 3
println(listOf("a", "b", "c").joinToString()) // a, b, c
}
扩展函数虽然可以像类的成员函数一样的方式调用,但是扩展函数并不允许打破类的封装性。
使用 Android Studio 的 Tools -> Kotlin -> Show Kotlin Bytecode,再点击 Decompile 按钮,查看 StringExtensions.kt
package com.kotlin.lib._1_topextensionfunction
fun String.lastChar(): Char {
return this.get(this.length - 1)
}
对应的 Java 代码:
public final class StringExtensionsKt {
public static final char lastChar(@NotNull String $this$lastChar) {
Intrinsics.checkNotNullParameter($this$lastChar, "$this$lastChar");
return $this$lastChar.charAt($this$lastChar.length() - 1);
}
}
可以看到,扩展函数只是看起来像是类的成员函数(在调用方式上),实质上是静态函数,它把调用对象作为了静态函数的第一个参数。
public class JavaTest {
public static void main(String[] args) {
System.out.println(StringExtensionsKt.lastChar("Java"));
}
}
可以看到,StringExtensions.kt
这个 kt 文件名,对应的 Java 类是 StringExtensionsKt
,可以通过这个类名调用内部的静态函数 lastChar
,调用者是作为静态函数的第一个参数传入的。
我们知道,重写成员函数是很常见的,但是,扩展函数是不可以重写的。
定义两个类,View
类及其子类 Button
:
open class View
class Button: View()
分别给 View
类型和 Button
类型定义扩展函数 showOff
:
fun View.showOff() = println("View extension showOff")
fun Button.showOff() = println("Button extension showOff")
调用:
fun main() {
val buttonView: View = Button()
buttonView.showOff()
}
可以看到,showOff
是高亮显示的
打印日志:
View extension showOff
可以看到,虽然 View
类型和 Button
类型都定义扩展函数 showOff
,但是打印是取决于变量的静态类型,而不是变量的运行时类型,比如:val buttonView: View = Button()
这个变量的静态类型是 View
类型,运行时类型是 Button
类型,调用的是 View
的扩展函数:fun View.showOff() = println("View extension showOff")
。也就是说,调用哪个扩展函数,取决于接收者的静态类型,而不是接收者的运行时类型。
这是为什么呢?在前面我们学习到扩展函数实质上是静态函数。这里再去看一下调用对应的 Java 字节码:
public final class TestKt {
public static final void main() {
View buttonView = (View)(new Button());
ViewExtenionsKt.showOff(buttonView);
}
}
它们的扩展函数对应的 Java 代码如下:
public final class ViewExtenionsKt {
public static final void showOff(@NotNull View $this$showOff) {
Intrinsics.checkNotNullParameter($this$showOff, "$this$showOff");
String var1 = "View extension showOff";
boolean var2 = false;
System.out.println(var1);
}
public static final void showOff(@NotNull Button $this$showOff) {
Intrinsics.checkNotNullParameter($this$showOff, "$this$showOff");
String var1 = "Button extension showOff";
boolean var2 = false;
System.out.println(var1);
}
}
可以看到,一个 View
类型的 buttonView
变量,实际上会作为静态函数的参数传入,会匹配到 public static final void showOff(@NotNull View $this$showOff)
这个静态函数,所以打印的是 "View extension showOff"
。
在 View
类及其子类 Button
增加和扩展函数同签名的成员函数:
open class View {
open fun showOff() {
println("View member showOff" )
}
}
class Button: View() {
override fun showOff() {
println("Button member showOff" )
}
}
调用:
fun main() {
val buttonView: View = Button()
buttonView.showOff()
}
在 AS 中,showOff
和之前的颜色不一样了:
打印日志:
Button member showOff
成员函数会被优先使用。这就说明:给类添加一个和扩展函数同样签名的成员函数,那么对应类定义的消费者将会重新编译代码,开始指向新的成员函数。实际上,这种情况下,扩展函数是永远不会再被调用的。
如果扩展函数只是和成员函数的函数名字相同,参数列表不同,这种情况下,二者是不会干扰的。
扩展函数的接收者,是表明哪个接收者接收了这个扩展函数,就只能由那个类型的对象才调用这个函数。
实际上,扩展函数是一个顶层函数,它不属于任何类,当然也不属于接收者。
接收者的作用是限制只有通过接收者类型的对象才可以调用这个扩展函数。
接收者只拥有扩展函数的调用权,而不是扩展函数的所有者。
接收者接收了什么呢?接收者接收了扩展函数的调用权而已,是扩展函数的设计者把这个扩展函数的调用权给了接收者。
比如,开头定义的扩展函数:
package com.kotlin.lib._1_topextensionfunction
fun String.lastChar(): Char {
return this.get(this.length - 1)
}
String
这个接收者类型,就限定了 lastChar()
这个扩展函数,只能通过 String
类型的对象来调用,而不可以通过 String?
类型或者 Int
类型等其他类型来调用。但是,lastChar
扩展函数并不属于 String
。
在 Kotlin 中,和 Java8 一样,只有把函数转换成一个值,才可以传递它。这也就是说,函数并不是一个值。
那么,如何转换呢?使用::(双冒号)运算符来转换。
对于一个顶层函数 greeting
来说:
package com.kotlin.lib._1_topextensionfunction
fun greetings(message: String) {
println("Hello, $message")
}
使用::(双冒号)运算符来转换:
fun main() {
val greeting = ::greetings
}
这里变量 greeting
是使用类型推断的,那么显式的类型是什么呢?
在 As 中,把鼠标放在 greeting
变量上,按下 Alt + Enter,在弹出菜单中选择 Specify type explicitly,来显式地指定类型:
接着弹出一个类型列表供选择:
这里我们选择 (message: String) -> Unit
这个类型,因为 Any
类型在这里不能太宽了,而其余的类型都是基于 Kotlin 反射的。
fun main() {
val greeting: (message: String) -> Unit = ::greetings
}
(message: String) -> Unit
是一个函数类型,括号中的是函数参数类型,紧接着是一个箭头,箭头后面是函数的返回类型。
函数类型的参数名是可以省略的:
fun main() {
val greeting: (String) -> Unit = ::greetings
}
说了顶层函数的引用方式,那么扩展函数如何引用呢?
定义两个扩展函数:
fun String.greetings2() {
println("Hello, $this")
}
fun String?.greeting3() {
println("Hello, $this")
}
直接使用双冒号运算符来转换是不可以的,必须在双冒号运算符前加上接收者类型。
fun main() {
val greeting2 = String::greetings2
val greeting3 = String?::greeting3
}
需要特别说明双冒号运算符前面加的是接收者类型,而不是接收者类。对于 greeting3
的引用,写为String::greeting3
也是正确的,但是这样就把 greeting3
的调用者类型收窄了。
对于 String?::greeting3
这样的函数引用,允许传入可空类型和非空类型:
val greeting3 = String?::greeting3
greeting3("Kotlin")
greeting3(null)
对于 String::greeting3
这样的函数引用,只允许传入非空类型:
val greeting3 = String::greeting3
greeting3("Kotlin")
greeting3(null) // 编译报错:Null can not be a value of a non-null type String
收窄是正确的,但是放宽是不可以的:
// 编译报错:Only safe (?.) or non-null asserted (!!.) calls are
// allowed on a nullable receiver of type String?
val greeting2: Any = String?::greetings2
通过函数引用,可以收窄调用者类型,在某些情况下,或许是有作用的。
说完了接收者类型,我们接着看扩展函数的引用的类型是什么?仍然使用上面的显式指定类型的办法,得到:
val greeting2: Any = String::greetings2
好吧,As 不能帮到我们了。
但是,在定义扩展函数 greeting2
的时候,本来是打算使用 greeting
这个函数名的,编译报错了,我才改成 greeting2
这个名字的,现在看看报错信息吧:
package com.kotlin.lib._1_topextensionfunction
/*
编译报错:
Platform declaration clash: The following declarations have the same JVM signature (greetings(Ljava/lang/String;)V):
public fun greetings(message: String): Unit defined in com.kotlin.lib._1_topextensionfunction in file Util.kt
public fun String.greetings(): Unit defined in com.kotlin.lib._1_topextensionfunction in file Util.kt
*/
fun greetings(message: String) { // 函数声明下有红色波浪线
println("Hello, $message")
}
fun String.greetings() { // 函数声明下有红色波浪线
println("Hello, $this")
}
翻译一下:
Platform declaration clash: The following declarations have the same JVM signature (greetings(Ljava/lang/String;)V):
平台声明报错:如下的声明有相同的 JVM 签名 (greetings(Ljava/lang/String;)V)
在 JVM 看来,fun greetings(message: String)
和 fun String.greetings()
的签名是一样的,而一样的签名是不允许的,所以报错了。我们再去看看对应的 Java 字节码:
public final class UtilKt {
public static final void greetings(@NotNull String $this$greetings) {
Intrinsics.checkNotNullParameter($this$greetings, "message");
String var1 = "Hello, " + $this$greetings;
boolean var2 = false;
System.out.println(var1);
}
public static final void greetings(@NotNull String $this$greetings) {
Intrinsics.checkNotNullParameter($this$greetings, "message");
String var1 = "Hello, " + $this$greetings;
boolean var2 = false;
System.out.println(var1);
}
}
果然是一样的吧。
既然是一样的,那么 greeting2
变量的函数类型和 greeting
变量的函数类型是不是也是一样的呢?我们把
val greeting2: Any = String::greetings2
val greeting3: Any = String?::greeting3
修改为
val greeting2: (String) -> Unit = String::greetings2
val greeting3: (String?) -> Unit = String?::greeting3
编译是 OK 的。
如何使用扩展函数的引用呢?
fun main() {
val greeting2: (String) -> Unit = String::greetings2
greeting2.invoke("Kotlin") // 打印:Hello, Kotlin
greeting2("Android") // 打印:Hello, Android
}
可以看到,通过使用函数引用,和使用原函数一样,都可以正常调用。但是,它们的调用方式却有些不同:
"Jetpack".greetings2() // 打印:Hello, Jetpack
这是为什么呢?我们留到 2.3 小节再来看这个问题吧。
除了声明顶层扩展函数,Kotlin 还允许在类中声明扩展函数,这样的扩展函数既是它所在类的成员,又是某些其他类型的扩展。这样的函数就叫做成员扩展函数。
成员扩展函数就是在一个类中为另外一个类声明扩展函数。在这样一个扩展中,有多个隐式的接收者(即不需要限定符就可以访问其成员的对象):扩展函数声明所在类的实例被称为分发接收者(dispatcher receiver),扩展函数的接收者类型的实例被称为扩展接收者(extension receiver)。
这里展示一个成员扩展函数的例子:
class PhoneNumber(val number: String) {
fun isValid(): Boolean {
return number.length == 11 && number.all { it.isDigit() }
}
}
class PhoneBook {
fun verify(phoneNumber: PhoneNumber): Boolean {
return phoneNumber.check()
}
// check 是一个成员扩展函数
fun PhoneNumber.check(): Boolean {
printPhoneNumber(this.number) // printPhoneNumber 是由分发接收者,即 PhoneBook 对象来调用的。
return isValid() // isValid() 是由扩展接收者,即 PhoneNumber 对象来调用的。
}
private fun printPhoneNumber(number: String) {
println("PhoneBook: $number")
}
}
fun main() {
println(PhoneBook().verify(PhoneNumber("13912345678")))
}
打印:
PhoneBook: 13912345678
true
在我们的例子中,在 PhoneNumber
类中也声明一个和 PhoneBook
的 printPhoneNumber
一样的方法:
class PhoneNumber(val number: String) {
fun isValid(): Boolean {
return number.length == 11 && number.all { it.isDigit() }
}
fun printPhoneNumber(number: String) {
println("PhoneNumber: $number")
}
}
这时查看 PhoneBook
中的 printPhoneNumber
方法,已经变成灰色的,说明不再被调用了;而在 PhoneNumber
中增加的 printPhoneNumber
方法,已经变成高亮的,说明被调用了。
运行程序,查看日志:
PhoneNumber: 13912345678
true
可以知道, PhoneNumber
中增加的 printPhoneNumber
方法确实被调用了。
所以,当分发接收者和扩展接收者的成员之间出现命名冲突时,则会优先使用扩展接收者的成员。
那么怎样让 PhoneBook
中的 printPhoneNumber
方法被调用,而不调用 PhoneNumber
中的 printPhoneNumber
方法呢?
在调用 printPhoneNumber
时,前面加上 this@PhoneBook
:
fun PhoneNumber.check(): Boolean {
this@PhoneBook.printPhoneNumber(this.number)
return isValid()
}
这时查看 PhoneBook
中的 printPhoneNumber
方法,已经变成高亮的,说明被调用了;而在 PhoneNumber
中增加的 printPhoneNumber
方法,已经变成灰色的,说明不再被调用了。
运行程序,查看日志:
PhoneBook: 13912345678
true
可以知道, PhoneBook
中的 printPhoneNumber
方法确实被调用了。
成员扩展可以声明为 open
,并可以在子类中重写,这就是说对于分发接收者来说,当是由子类来分发时,就会调用子类重写的成员扩展函数;但是对于扩展接收者来说,仍然是静态解析的:哪个接收者对象来调用扩展函数,实际上就会调用以那个接收者为接收者的扩展函数。
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")
}
fun call(b: Base) {
b.printFunctionInfo() // call the extension function
}
}
class DerivedCaller: BaseCaller() {
override fun Base.printFunctionInfo() {
println("Base extension function in DerivedCaller")
}
override fun Derived.printFunctionInfo() {
println("Derived extension function in DerivedCaller")
}
}
fun main() {
BaseCaller().call(Base())
DerivedCaller().call(Base())
DerivedCaller().call(Derived())
}
打印:
Base extension function in BaseCaller
Base extension function in DerivedCaller
Base extension function in DerivedCaller
DerivedCaller().call(Base())
这行,是子类分发接收者 DerivedCaller
对象来分发,所以会调用 DerivedCaller
重写的成员扩展函数;扩展接收者是 Base
类型的,所以会调用 DerivedCaller
的 override fun Base.printFunctionInfo()
函数,打印:"Base extension function in DerivedCaller"
。
DerivedCaller().call(Derived())
这行,是子类分发接收者 DerivedCaller
对象来分发,所以会调用 DerivedCaller
重写的成员扩展函数;虽然扩展接收者实际上是一个 Derived
对象,但是它的静态类型是 Base
类型的,所以还是会 DerivedCaller
的 override fun Base.printFunctionInfo()
函数,打印:"Base extension function in DerivedCaller"
。
是不是还有些疑问呢?
我们一起去看看对应反编译后的 Java 代码吧,为了便于阅读,对 Java 代码进行了一些删减和整理:
package com.kotlin.lib._2_memberextensionfunction.decompiled;
class Base {
}
final class Derived extends Base {
}
class BaseCaller {
public void printFunctionInfo(@NotNull Base base) {
System.out.println("Base extension function in BaseCaller");
}
public void printFunctionInfo(@NotNull Derived derived) {
System.out.println("Derived extension function in BaseCaller");
}
public final void call(@NotNull Base b) {
this.printFunctionInfo(b);
}
}
final class DerivedCaller extends BaseCaller {
public void printFunctionInfo(@NotNull Base base) {
System.out.println("Base extension function in DerivedCaller");
}
public void printFunctionInfo(@NotNull Derived derived) {
System.out.println("Derived extension function in DerivedCaller");
}
}
public final class CallerKt {
public static void main(String[] var0) {
new BaseCaller().call(new Base());
new DerivedCaller().call(new Base());
new DerivedCaller().call(new Derived());
}
}
运行代码,查看日志:
Base extension function in BaseCaller
Base extension function in DerivedCaller
Base extension function in DerivedCaller
可以看到,和之前的 Kotlin 代码,执行结果是一样的。
看一下 Kotlin 代码和 Java 代码的对应关系,Java 代码是把 Kotlin 代码成员扩展函数的接收者,作为第一个参数传入了。
比较项 | 顶层扩展函数 | 成员扩展函数 |
---|---|---|
编译后的Java函数 | 静态函数 | 成员函数 |
作用域 | 可以在任何地方调用 | 只可以在声明它的类中调用 |
函数引用 | 可以引用 | 不可以引用 |
可以重写? | 不可以 | 可以 |
接收者 | 存在一个扩展接收者 | 存在一个分发接收者和一个扩展接收者(这点确实会另开发者感到迷惑) |
在 2.1节的最后,我们留下了一个疑问,在本节就可以解决了。
以接收一个 String
类型参数和一个函数类型参数的高阶函数为例:
fun printChar1(str: String, block: (String) -> Char) {
println(block(str))
}
这个函数的作用是:把字符串作为参数传递给一个函数类型对象,返回一个字符,并打印这个字符。
可以传入一个扩展函数的引用:
package com.kotlin.lib._1_topextensionfunction
fun String.lastChar(): Char {
return this.get(this.length - 1)
}
调用:
fun main() {
val lastChar: (String) -> Char = String::lastChar
printChar1("Kotlin") {
lastChar(it)
}
}
打印:
n
这里获取扩展函数的引用和我们在 2.1 节中的方式是一样的,都是获取了函数类型。
实际上,可以将函数类型转换成扩展函数类型,或者说可以将 lambda 转换成带接收者的 lambda。
As 是支持这种转换的,把光标放在 printChar1
函数的 (String)
位置上,按下 Alt + Enter 快捷键,会弹出一个菜单:
选择第二项:Convert ‘(String) -> Char’ to ‘String.() -> Char’,得到的 printChar1
为:
fun printChar1(str: String, block: String.() -> Char) {
println(str.block())
}
为了比较这两种类型,把转换后的写成 printChar2
,printChar1
仍保持转换前的形式:
fun printChar2(str: String, block: String.() -> Char) {
println(str.block())
}
调用:
fun main() {
val lastChar: (String) -> Char = String::lastChar
printChar1("Kotlin") {
lastChar(it)
}
printChar2("Android") {
// 可以看到,这里就可以和扩展函数一样的方式调用了。
this.lastChar()
}
}
打印:
n
d
我们把这样的函数类型,如(String) -> Char
,称为普通函数类型;把这样的函数类型,如String.() -> Char
,称为扩展函数类型。
普通函数类型是如何转换成扩展函数类型的呢?
将普通函数类型参数列表中的第一个参数移到括号外边,并用一个.(点)与其他的参数分隔开,这样就得到了对应的扩展函数类型。
这种转换,反过来也是可以的,也就是说,可以把一个扩展函数类型转换为一个普通函数类型。
回调 2.1 节中的疑问:
// 使用普通函数类型
val greeting2: (String) -> Unit = String::greetings2
greeting2.invoke("Kotlin")
greeting2("Android") // 以普通函数的方式来调用
// 使用扩展函数类型
val greeting22: String.() -> Unit = String::greetings2
"Kotlin".greeting22() // 以扩展函数的方式来调用
在实际开发中,对于顶层扩展函数的使用比较多;对于成员扩展函数来说,它比顶层扩展函数有了作用域的限制,也带来一些弊端,它主要的作用是应用在 DSL 中。扩展函数虽然看起来很好用,但是我们不应该盲目使用。关于这些,本文并没有涉及,同学们可以查看本文的参考链接,继续学习。
本文的代码已经上传到 github,方便大家结合代码学习。
会写「18.dp」只是个入门——Kotlin 的扩展函数和扩展属性(Extension Functions Properties)
扔物线的文章带视频,讲得非常深入,多学几遍。
Kotlin官方文档-Extensions
官方文档,值得仔细阅读。但是,觉得官网里面的例子不怎么好。
Kotlin 扩展
这是 Kotlin 官方文档扩展的中文版本了。
Kotlin dilemma: Extension or Member
这篇文章说明了什么时候该用扩展,什么时候不该用扩展。这是本文没有涉及的内容,因为笔者自己也在学习这块。
Effective Kotlin Item 46: Avoid member extensions
这篇文章说明了成员扩展应该尽量避免使用,DSL的情况 除外。