Kotlin标准库函数
Kotlin提供了一个标准函数库,例如run, with, let, also, apply等函数,开发中使用十分方便。
我们可以通过源码来观察学习这些函数
先来看看run函数
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
run 函数使用泛型通配符 T 和 R ,对 T 进行方法扩展,传入一个 block 函数 ,返回 R 。
逐步分析代码
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
contract是Kotlin1.3的新特性,目前还是处于实验性阶段,即API在稳定版之前可能会发生变动。
它叫做Kotlin的契约,是面向编译器的,为编译器提供稳定的方法行为,告诉编译器我要做什么。
Kotlin 编译器会做大量的静态分析工作,以提供警告并减少模版代码。其中最显著的特性之一就是智能转换——能够根据类型检测自动转换类型。
fun foo(s: String?) {
if (s != null) s.length // 编译器自动将“s”转换为“String”,而不是String?
}
然而,一旦将这些检测提取到单独的函数中,所有智能转换都立即消失了:
fun String?.isNotNull(): Boolean = this != null
fun foo(s: String?) {
if (s.isNotNull()) s.length // 没有智能转换,这行代码会编译不过
//因为s不确定是不是String
}
所以为了改善在此类场景中的行为,Kotlin 引入了契约这个概念。
run函数里面的契约代码的含义是指会在这里调用 block 函数,而且只调用一次。
contract有多个描述符:
- returns(): 描述函数正常返回(无返回值)但没有抛出任何异常的情况
- returns(value: Any?): 描述函数以指定的return [value]正常返回的情况
- returnsNotNull(): 描述函数正常返回任何非“null”值的情况
- callsInPlace: 用于在适当的位置调用函数参数[lambda],而且可以指定调用次数
函数调用次数也有相关参数:
InvocationKind
- AT_MOST_ONCE: 函数参数将被调用一次或根本不被调用
- AT_LEAST_ONCE: 函数参数将被调用一次或多次
- EXACTLY_ONCE: 函数参数将被调用一次
- UNKNOWN: 函数参数就地调用,但不知道可以调用多少次
我们也可以通过 contract 进行自定义契约,可以为自己的函数声明契约
通过调用标准库(stdlib)函数 contract
来引入自定义契约,该函数提供了 DSL 作用域:
fun String?.isNullOrEmpty(): Boolean {
contract {
returns(false) implies (this@isNullOrEmpty != null)
}
return this == null || isEmpty()
}
了解了Kotlin的契约之后,我们再来看看一开始的run函数
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
//契约代码,不影响业务逻辑
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
我们跳过契约代码,可以发现run函数其实就是返回了 block 函数,即是返回了 block 函数的返回值。
看一下示例代码
var text = activityMainBinding.run {
this.rvContent.invalidate()
this.tvTitle.text = "abc"
this.tvTitle.text
}
因为 block 本身就是 T.() 扩展函数,所以可以拿到 T 的对象
let 函数
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
可以看见let函数跟run函数不一样的地方在于,block函数是传入了调用者 T 的对象
看一下示例代码
var test = activityMainBinding.let {
it.rvContent.invalidate()
it.tvTitle.text = "abc"
it.tvTitle.text
}
这个 it 则是 block 函数传入的 T 对象,it 只是个别名,是可以自己修改的
apply函数
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
apply 函数是调用了一个没有返回值的 block 函数,而且返回了当前对象。
看一下示例代码
activityMainBinding.apply {
this.rvContent.invalidate()
this.tvTitle.text = "abc"
}.tvTitle.text = "abc"
also函数
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}
also函数跟apply函数类似,唯一不同的是传入的 block 函数是带有调用对象的。
看一下示例代码
activityMainBinding.also {
it.rvContent.invalidate()
it.tvTitle.text = "abc"
}.tvTitle.text = "abc"
it 同样也是一个别名
with函数
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}
可以看见with函数不是扩展函数,所以它会接收一个调用者的对象和 block 函数,实质是用这个对象调用 block 函数,有点像代理模式。
inline关键字
我们会发现,上面的方法前面都有个关键字 inline ,它属于 Kotlin 的高级特性——内联函数。
调用一个方法其实就是一个方法压栈和出栈的过程,调用方法时将栈帧压入方法栈,然后执行方法体,方法结束时将栈帧出栈,这个压栈和出栈的过程是一个耗费资源的过程,而且如果调用过多次,方法栈空间被耗尽,没有足够资源分配给新创建的栈帧,就会抛出 java.lang.StackOverflowError 错误。
为了避免遇到这种情况,所以 Kotlin 提出了内联函数的使用。
内联函数是指被inline标记的函数,其原理就是:在编译时期,把调用这个函数的地方用这个函数的方法体进行替换。
举个例子:
我们先定义了一个普通方法
fun show() {
val a = "shadow:"
val b = "hhhh"
println(a + b)
}
然后在 Activity onCreate()调用
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val activityMainBinding = ActivityMainBinding.inflate(LayoutInflater.from(this))
setContentView(activityMainBinding.root)
show()
}
通过 Kotlin 编译成字节码,再反编译,看看其.java文件
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding activityMainBinding = ActivityMainBinding.inflate(LayoutInflater.from((Context)this));
Intrinsics.checkExpressionValueIsNotNull(activityMainBinding, "activityMainBinding");
this.setContentView(activityMainBinding.getRoot());
this.show();
}
public final void show() {
String a = "shadow:";
String b = "hhhh";
String var3 = a + b;
boolean var4 = false;
System.out.println(var3);
}
发现就是正常调用了 show()
那我们再来看看加了关键字 inline 的 show()
inline fun show() {
val a = "shadow:"
val b = "hhhh"
println(a + b)
}
进行反编译
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding activityMainBinding = ActivityMainBinding.inflate(LayoutInflater.from((Context)this));
Intrinsics.checkExpressionValueIsNotNull(activityMainBinding, "activityMainBinding");
this.setContentView(activityMainBinding.getRoot());
int $i$f$show = false;
String a$iv = "shadow:";
String b$iv = "hhhh";
String var7 = a$iv + b$iv;
boolean var8 = false;
System.out.println(var7);
}
public final void show() {
int $i$f$show = 0;
String a = "shadow:";
String b = "hhhh";
String var4 = a + b;
boolean var5 = false;
System.out.println(var4);
}
可以发现在编译时期就会把方法内容替换到调用该方法的地方,这样就会减少方法压栈,出栈,进而减少资源消耗。
这就是内联函数的作用,inline 关键字实际上增加了代码量,但是提升了性能,而且增加的代码量是在编译期执行的,对程序可读性不会造成影响。