1 前言
接触 Kotlin 的扩展函数有一段时间了,不过对这个知识的理解只是停留在顶层扩展函数而已。
在继续学习 Kotlin 的使用时,发现这样的理解是远远不够的,比如这些问题就不清楚:
- 扩展函数的本质是什么吗?
- 如何引用一个扩展函数?
- 成员扩展函数是什么,有什么用?
- 扩展函数类型和普通函数类型是什么,如何相互转换?
本文会一一演示说明并解决这些问题,现在占用同学们几分钟时间,我们一起开始吧。
2 正文
2.1 (顶层)扩展函数
在 2.1 下,我们把顶层扩展函数简称为扩展函数。
2.1.1 声明(顶层)扩展函数
扩展函数是定义在类的外面,这里定义一个 String 类的扩展函数,用来获取字符串的最后一个字符:
package com.kotlin.lib._1_topextensionfunction
fun String.lastChar(): Char {
return this.get(this.length - 1)
}
把要扩展的类或者接口的名称,放到即将添加的函数前面。这个类或者接口就被称为接收者类型;用来调用这个扩展函数的那个对象,叫作接收者对象。如下图所示:
使用定义好的扩展函数:
fun main() {
println("Kotlin".lastChar())
}
可以看到,这个扩展函数是符合预期的。在这次调用中,String 是接收者类型,而 "Kotlin" 就是接收者对象。
从调用上看,调用lastChar() 和调用 String 类的普通成员函数的方式是一模一样的,都是通过对象.方法名的方式调用的。
另外,这里的扩展函数,可以像普通的成员函数一样,省略掉 this :
package com.kotlin.lib._1_topextensionfunction
fun String.lastChar(): Char {
return get(length - 1)
}
2.1.2 对扩展函数的深入理解
接收者类型就只是类或者接口吗?
在 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())
println(null.firstChar())
}
可以对 List<Int> 类型,List<Double> 类型定义求所有元素之和的扩展函数 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())
println(listOf(1.1, 2.2, 3.3).sum())
}
可以对 List<T> 定义一个把所有元素以逗号拼接为字符串的扩展函数,这也是一个泛型扩展函数:
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())
println(listOf("a", "b", "c").joinToString())
}
扩展函数能访问类私有的或者受保护的成员吗?
扩展函数虽然可以像类的成员函数一样的方式调用,但是扩展函数并不允许打破类的封装性。
使用 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);
}
}
可以看到,扩展函数只是看起来像是类的成员函数(在调用方式上),实质上是静态函数,它把调用对象作为了静态函数的第一个参数。
从 Java 中如何调用扩展函数?
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)
收窄是正确的,但是放宽是不可以的:
val greeting2: Any = String?::greetings2
通过函数引用,可以收窄调用者类型,在某些情况下,或许是有作用的。
说完了接收者类型,我们接着看扩展函数的引用的类型是什么?仍然使用上面的显式指定类型的办法,得到:
val greeting2: Any = String::greetings2
好吧,As 不能帮到我们了。
但是,在定义扩展函数 greeting2 的时候,本来是打算使用 greeting 这个函数名的,编译报错了,我才改成 greeting2 这个名字的,现在看看报错信息吧:
package com.kotlin.lib._1_topextensionfunction
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")
greeting2("Android")
}
可以看到,通过使用函数引用,和使用原函数一样,都可以正常调用。但是,它们的调用方式却有些不同:
"Jetpack".greetings2()
这是为什么呢?我们留到 2.3 小节再来看这个问题吧。
2.2 成员扩展函数
2.2.1 声明成员扩展函数
除了声明顶层扩展函数,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()
}
fun PhoneNumber.check(): Boolean {
printPhoneNumber(this.number)
return isValid()
}
private fun printPhoneNumber(number: String) {
println("PhoneBook: $number")
}
}
fun main() {
println(PhoneBook().verify(PhoneNumber("13912345678")))
}
打印:
PhoneBook: 13912345678
true
2.2.2 对成员扩展函数的深入理解
当分发接收者和扩展接收者的成员之间出现命名冲突时,会优先使用哪个成员?
在我们的例子中,在 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()
}
}
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.3 扩展函数类型
在 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()
3 最后
在实际开发中,对于顶层扩展函数的使用比较多;对于成员扩展函数来说,它比顶层扩展函数有了作用域的限制,也带来一些弊端,它主要的作用是应用在 DSL 中。扩展函数虽然看起来很好用,但是我们不应该盲目使用。关于这些,本文并没有涉及,同学们可以查看本文的参考链接,继续学习。
本文的代码已经上传到 github,方便大家结合代码学习。
4 参考
|