Kotlin的一大主题就是平滑集成现有的代码。即便纯Kotlin项目是建立在诸如JDK、Android框架和其他第三方框架等Java库之上的。当你把Kotlin整合到Java项目中去时,你也要处理已经或者未转换成Kotlin的现有代码。当使用这些API时可以使用全部Kotlin最有趣的特性而无需重新编写这些代码,这会是一件很好的事情吗?那就是扩展函数能让你做的事情。 概念上讲,一个扩展函数是这样一个东西:它是一个可以作为一个类成员进行调用的函数,但是定义在这个类的外部。为了解释这个概念,让我们添加一个方法来计算一个字符串的最后一个字符:
package strings
fun String.lastChar(): Char = this.get(this.length - 1)
正如你所见,你所需要做的仅仅是在你添加的函数的名字之前放置你需要扩展的类或者接口的名字。这个类名叫做接收器类型(receiver type),而你调用的扩展函数的值叫做接收器对象(receiver object)。详细解释如图3.1:
图3.1 接收器类型是扩展定义的类型,同时,接收器对象是这个类型的实例
你可以使用跟普通类成同样的语法来调用这个函数:
>>> println("Kotlin".lastChar())
n
在这个例子中,String
是接收器类型,同时"Kotlin"是接收器对象。
在某种意义上,你已经添加了你的方法到String
类。尽管String
并不是你的代码的一部分,你可能甚至没有那个类的远吗,你任然可以用你的项目中所需的方法来扩展它。甚至String
是否用Java编写的也没无关紧要,Kotlin或者其他JVM语言,例如Groovy。只要它被编译成了Java类,你可以添加到你自己的扩展到那个类中。
在一个扩展函数的主体中,你可以使用this
(关键字)就像你在一个方法中使用它那样。作为一个常规方法,你可以忽略它:
package strings
fun String.lastChar(): Char = get(length - 1) // 1 "this"引用是隐式的
在这个扩展函数中,你可以直接访问你扩展的类的函数和属性,就像在定义在这个类中的方法那样。注意扩展函数并不允许你打破封装。跟定义在类中的方法不同,扩展函数并不能访问私有或保护访问属性的类成员。
3.3.1 导入和扩展函数
当你定义一个扩展函数是,它并不会自动在你的整个项目中变为可用。相反的,它需要被导入,就像其他的类或者函数那样。这有助于避免意外的命名冲突。Kotlin允许你使用你用于类的同样单独语法来导入单独的函数:
import strings.lastChar
val c = "Kotlin".lastChar()
当然,使用通配符(*
)导入也是可以的:
import strings.*
val c = "Kotlin".lastChar()
你可以使用as
关键词来该改变你所导入的类或者函数的名字:
import strings.lastChar as last
val c = "Kotlin".last()
当你在不同的包中有多个同名的函数并且你想在同一个文件中使用它们时,在导入中改变一个名字是非常有用的,对于常规的类和函数,你在这种情况下有另外的选择:你可以使用完全有效的名字来有用类或者函数。对于扩展函数,这个语法要求你使用缩写名字,因此一个导入声明中的as
关键词是解决冲突的唯一方法。
3.3.2 从Java中调用扩展函数
调用一个扩展函数并没有涉及适配器对象的创建或者任何其他运行时开销。在底层方面,一个扩展函数是一个接受接收器对象作为第一个参数的静态方法。
这让(我们)从Java中使用扩展函数变得非常容易:你调用静态方法并传递接收器对象实例。就像和其他顶层函数,包含这个方法的java类的名字由声明这个函数的文件的名字决定。我们可以把代码声明在一个StringUtil.kt
文件中:
/* Java */
char c = StringUtilKt.lastChar("Java");
这个扩展函数被声明为顶层函数,因此它被编译为一个静态方法。你从Java中静态的可以导入lastChar
方法,用法简化为"lastChar
"。比起Kotlin版本来,这份代码的可读性多少有点下降了,但是从Java的角度来说这是符合语言习惯的。
3.3.3
现在你可以编写最终版本的joinToString
函数了。这几乎就是你将在Kotlin标准库中找到的(代码):
fun <T> Collection<T>.joinToString( // 1 用Collection<T>声明一个扩展函数
separator: String = ", ", // 2 为参数声明默认值
prefix: String = "", // 2
postfix: String = "" //2
): String {
val result = StringBuilder(prefix)
for ((index, element) in this.withIndex()) { // 3 "this"指向接收器对象:T类型的集合
if (index > 0) result.append(separator)
result.append(element)
}
result.append(postfix)
return result.toString()
}
你为一个集合声明了一个扩展,同时为所有的参数提供了默认值。现在你可以像类的一个成员那样调用joinToString
:
>>> val list = arrayListOf(1, 2, 3)
>>> println(list.joinToString(" "))
1 2 3
由于扩展函数只是静态函数调用的高效的语法糖,你可使用一个更加具体的类型作为一个接收器类型而不仅仅是一个类。比如说你想要一个仅仅能被字符串集合调用的join
函数。用其他类型的一列对象来调用这个函数是无效的:
fun Collection<String>.join(
separator: String = ", ",
prefix: String = "",
postfix: String = ""
) = joinToString(separator, prefix, postfix)
>>> println(listOf("one", "two", "eight").join(" "))
one two eight
>!> listOf(1, 2, 8).join()
Error: Type mismatch: inferred type is List<Int> but Collection<String>
was expected.
扩展的静态类型也意味着扩展函数不能被子类覆盖(overridden)。让我们来看一个例子吧!
3.3.4 不可覆盖(overriding)的扩展函数
方法覆盖在Kotlin对平常的成员函数是有效的,但是你不能覆盖一个扩展函数。比如说你有两个类,View
和它的子类Button
,同时Button
类覆盖了来自父类的click
函数:
open class View {
open fun click() = println("View clicked")
}
class Button: View() { // 1 Button类继承自View类
override fun click() = println("Button clicked")
}
如果你声明了一个View
类型的变量,你也可以在这个变量中存储Button
类型的值,因为Button
是View
的子类。如果你用这个变量调用了一个常规方法,比如click()
,而且这个方法在Button
类中被覆盖了。这个来自Button
类的覆盖的实现将会被使用:
>>> val view: View = Button()
>>> view.click() // 1 Button类的示例的方法被调用。//AU:这个措辞正确吗?好像有点不对。TT
Button clicked
但是这种方式对于扩展来说是无效的,正如图3.2所示:
图3.2 扩展函数声明在类的外部
扩展函数并不是类的一部分。它们是声明在类的外部的。尽管你可以为某个基类和它的子类用同样的名字和参数类型来定义扩展函数,被调用的函数依赖于已被声明的静态类型的变量,而不是依赖于变量值的运行时类型。
接下来的例子展示了两个声明在View
和Button
类的showOff
扩展函数:
fun View.showOff() = println("I'm a view!")
fun Button.showOff() = println("I'm a button!")
>>> val view: View = Button()
>>> view.showOff() // 1 扩展函数被静态的方式进行解析
I'm a view!
当你用一个View
类型的变量调用showOff
时,对应的扩展将会被调用,尽管这个值的真实类型是Button
。
如果你重新调用了用Java编译成静态函数的扩展函数,这个行为对你来说应该是很清晰的,因为Java以同样的方式来选择函数:
/* Java */
>>> View view = new Button();
>>> ExtensionsKt.showOff(view); // 1 showOff函数被声明在extensions.kt文件中
I'm a view!
如你所见,覆盖对于扩展函数来说是不起作用的:Kotlin以静态解析它们。
注意 如果类有一个成员函数跟一个扩展函数有着相同的签名,成员函数总是优先的。当你扩展类的API时,你应该记住这一点:如果你添加了一个跟你已定义类的客户端(调用者)的扩展函数具有同样的签名成员函数,同时客户端随后重新编译了他们的代码,它将会改变它的含义并开始指向一个新的成员函数。
我们已经讨论了如何为外部的类提供额外的方法。现在让我们来看看如何对属性做同样的事。
3.3.5 扩展属性(Extension properties)
扩展属性提供了一种方法用能通过属性语法进行访问的API来扩展类。而不是函数的语法。尽管它们被叫做属性,它们不能拥有任何的状态:它不可能添加额外的字段到现有的Java对象实例。但是更简短的语法在某些时候任然是很方便的。
在前一个章节,你定义了一个函数lastChar
。现在让我们把它转换成一个属性:
val String.lastChar: Char
get() = get(length - 1)
你可以看到,就像使用函数一样,一个扩展属性看起来就像一个加了一个接收器类型的常规属性。访问器(getter)必须始终有定义,因为此处没有备用的字段,所以没有默认的访问器实现。出于同样的原因初始化器也是不允许(存在)的:这里并没有任何地方来存储指定为初始化器的值。
如果你用StringBuilder
定义了同样的属性,你可以让它为var
类型,因为StringBuilder
的内容可以被修改:
var StringBuilder.lastChar: Char
get() = get(length - 1) // 1 属性的访问器
set(value: Char) { // 2 属性的设置器
this.setCharAt(length - 1, value)
}
你访问扩展属性(的方式)跟成员属性完全一样:
>>> println("Kotlin".lastChar)
n
>>> val sb = StringBuilder("Kotlin?")
>>> sb.lastChar = '!'
>>> println(sb)
Kotlin!
注意,当你需要从Java范文一个扩展属性时,你应该显式的调用它的访问器:StringUtilKt.getLastChar("Java")
。
我们已经在普遍意义上讨论了扩展的概念。现在让我们回到集合的主题并看一些有助于你运用它们的库函数,以及在这些函数中涉及的语言特性。