扩展函数可以称为Kotlin的核心,标准库里到处充斥着扩展函数和高阶函数。然而标准库中扩展函数看起来都是高深莫测,一脸懵逼。本文教你如何理解扩展函数。
Kotlin同C#类似,能够扩展一个类的新功能而无需继承该类。通俗点讲就是使用扩展函数,可以为无法修改源代码的对象添加新的方法,或者强制让对象支持某些方法,添加之后这些方法看起来就是对象本来就有的功能。而在Java中我们通常使用写Utils
的形式来完成这项功能,而Kotlin不需要。
声明一个扩展函数,我们需要指定一个接收者类型也就是被扩展的类型来作为他的前缀。例如我们可以对String
声明一个扩展函数为print()
:
fun String.print() {
println(this)
}
//你也可以简写成如下形式
fun String.print() = print(this)
//使用
"I'm String".print()
扩展函数的声明是有作用域的,默认是当前包及子包可以免
import
使用。比如你在com.wastrel
包下声明的,那么com.wastrel.a
下也是可以使用的。当然你也可以对扩展函数加以private
等属性来限制其作用域。如果你的声明是public
(默认即是公有的),那么也可以在它默认作用域外使用improt
来导入使用。
有时候你可能需要对很多类进行扩展,这时候可以对扩展函数进行泛化,例如:
//对所有List类型进行扩展print函数
fun <T> List<T>.print()
{
println(...)
}
//对所有继承至View的类进行扩展print函数
fun <T:View> T.print()
{
println(...)
}
//其实上面这种写法是没有意义的,因为扩展函数本身也会被其子类继承。即你给某一个类进行了函数扩展,那么他的子类也能使用这些扩展函数。
在Java中用一个空对象去调用成员方法是会引发NPE异常的,但是Kotlin中支持可空扩展,即允许null调用并返回值。
//当用一个空对象去调用时将返回“Null Object”。
fun Any?.string():String
{
if(this==null) "Null Object" else this.toString()
}
由上面的例子我们可以看到,当我们给View
增加扩展函数之后,其所有的子类均能使用print
方法。那么如果某一个子类本身有print
方法那么会调用哪一个呢?
//扩展函数是静态分发证实
open class A {
fun base() = "Base"
}
class B : A()
class C : A()
class D : A() {
fun foo() = "D"
fun string() = "I'm D"
}
fun A.foo() = "A"
fun B.foo() = "B"
fun B.base() = "Base B"
fun A?.string() = this?.javaClass ?: "NULL Object"
fun main(args: Array) {
println(A().foo()) //A
println(B().foo()) //B
println(C().foo()) //A
println(D().foo()) //D
//Warning Kotlin: Extension is shadowed by a member: public final fun base(): String
//此处不会调用扩展函数,将会调用父类A的base方法
println(B().base()) //Base
println(B().string())//class B
println(D().string())//I'm D
var d: D? = null
println(d.string())//NULL Object
}
1、对于非空对象,当扩展函数与类本身的成员函数(无论是子类成员函数还是父类成员函数)冲突时始终调用类本身成员函数。
2、对于可空扩展,当对象为空时调用扩展函数,当对象非空时遵循第一条。
3、当子类的扩展函数与父类扩展函数一致时,调用子类扩展函数。
/**
* Returns a list containing only elements matching the given [predicate].
*/
public inline fun Iterable.filter(predicate: (T) -> Boolean): List {
return filterTo(ArrayList(), predicate)
}
/**
* Appends all elements matching the given [predicate] to the given [destination].
*/
public inline fun > Iterable.filterTo(destination: C, predicate: (T) -> Boolean): C {
for (element in this) if (predicate(element)) destination.add(element)
return destination
}
看到这两个原型第一反应肯定是predicate
是什么玩意,其实predicate
类似于Java中的接口,只不过这里声明比较简洁,predicate: (T) -> Boolean
表示predicate
是一个Function
,这个Function
输入一个类型为T
的参数返回一个Boolean
,如果有使用RxJava的话,对这个会非常熟悉,其等价于RxJava中的Function
。在Kotlin中,可以将a.invoke()
简写成a()
,即此处的predicate(element)
等价于predicate.invoke(element)
,因此这个扩展函数的功能很容易理解,即将所有符合条件的元素装在一个新的集合里返回。
上面的函数还出现有一个关键字
inline
,该关键字表示这个函数在编译的时候会嵌入到调用的地方去。并且从上面的例子可以看出,一个扩展函数是可以调用另外一个扩展函数的。
什么叫复合?简单一点讲就是把两个连续调用的函数组合起来,当然不仅仅是组合起来那么简单。直接看代码:
//利用中缀表示法扩展函数 进行函数复合
val add5:Function1 = { i: Int -> i + 5f }
val pow:Function1 = { i: Float -> i * i*1.0 }
val add5Pow:Function1 = add5 andThen pow
val add5Pow1 = add5 andThen1 pow
add5Pow(2) //(2+5)^2=49
//表示对Function1声明了一个扩展方法andThen,该方法接收一个Function1返回Function1
//返回的方法会先使用自身对变量p1进行处理,再使用传入的function处理。而达成直接从P1->R的过程。
infix fun Function1.andThen(function: Function1): Function1 {
return fun(p1: P1): R {
return function(this(p1))
}
}
//上面复合扩展函数的极致写法
infix fun Function1.andThen1(function: (P2) -> R) = fun(p1: P1) = function(this(p1))
上面这段代码可能更不容易理解。其实变量声明中的类型是可以省略的,如果省略的话代码将会更难理解。代码使用了
infix
关键字,这个关键字声明的函数表示允许使用a funName b
这样使用,其实等价于a.funName(b)
。Kotlin在简洁这个方向走得很远。显示这是一个把P1->P2->R
变换成P1->R
的过程。
Kotlin还有一个Java没有的特性,也就是操作符重载。有了操作符重载我们可以愉快的将两个对象加起来变成另外一个对象。
class M(var i: Int) {
operator fun times(b: M) = M(i * b.i)
override fun toString(): String {
return i.toString()
}
}
operator fun M.plus(b: M) = M(this.i + b.i)
fun main(arg: Array<String>) {
val a = M(5)
val b = M(10)
print(a + b) //15
println(a * b) //50
}
这里的操作符重载其实是用一些列指定的函数来重定义操作符的功能,比如用
plus
来表示+
,用times
来表示×
。当你的类实现了或者扩展了这一类方法,则可以在使用时用操作符来代替。
其实这只是Kotlin的一个语法糖,从编译的角度来讲,a+b
其实是等价于Java中的:
static M add(M a,M b)
{
return new M(a.i+b.i);
}
//下面是a+b被编译成class代码之后的样子。第三句验证了上面的说明
ALOAD 1
ALOAD 2
INVOKESTATIC child/OperatorKt.plus (Lchild/M;Lchild/M;)Lchild/M;
POP
表中左边为使用形态,右边为其执行形态。
表达式 | 翻译为 |
---|---|
a+b | a.plus(b) |
a-b | a.minus(b) |
a*b | a.times(b) |
a/b | a.div(b) |
a%b | a.rem(b) |
a..b | a.rangeTo(b) |
a in b | b.contains(a) |
a !in b | !b.contains(a) |
a[i] | a.get(i) |
a[i, j] | a.get(i, j) |
a[i_1, ……, i_n] | a.get(i_1, ……, i_n) |
a[i] = b | a.set(i, b) |
a[i, j] = b | a.set(i, j, b) |
a[i_1, ……, i_n] =b | a.set(i_1, ……, i_n,b) |
a() | a.invoke() |
a(i) | a.invoke(i) |
a(i,j) | a.invoke(i, j) |
a(i_1,……,i_n) | a.invoke(i_1, ……,i_n) |
a +=b | a.plusAssign(b) |
a -=b | a.minusAssign(b) |
a *=b | a.timesAssign(b) |
a /=b | a.divAssign(b) |
a %=b | a.modAssign(b) |
a ==b | a?.equals(b) ?: (b === null) |
a !=b | !(a?.equals(b) ?: (b ===null)) |
a > b | a.compareTo(b) > 0 |
a < b | a.compareTo(b) < 0 |
a >=b | a.compareTo(b) >=0 |
a <=b | a.compareTo(b) <=0 |
从前面的分析与例子可以知道,Kotlin编写的程序如果在JVM上运行,其最终编译之后的形态不太可能超越Java中的特性,其所做的功能就是让编程人员的代码尽可能的简洁,而让编译器处理更多的事情。因此Kotlin在性能上不太可能比Java快,但其新的语法和各种各样的语法糖可以增加开发效率。当然Kotlin也遗弃了一些Java中的特性,特别是在静态变量和函数这一块。有人把Kotlin比作Android的swift,显然如果谷歌大力推广,Kotlin自然能代替Java成为第一开发语言,如果谷歌只是宣布支持,而不做任何事情,我想Kotlin在Android领域短时间还不会太流行。