扩展函数
高阶函数
内联函数
在上篇文章 偷师 - Kotlin 委托 里提到了 ViewBindingDelegate
库,经过 kotlin
委托的方式简化了在 Android
项目中 ViewBinding
使用。原本是不想再写 ViewBindingDelegate
分析的,可是项目中用到的 kotlin
知识点确实也有些是须要重点记录一下的。java
直接来看 vbpd-full/../ActivityViewBindings.kt
文件中的 viewBinding
方法:web
...
@JvmName("inflateViewBindingActivity")
public inline fun <reified T : ViewBinding> ComponentActivity.viewBinding(
createMethod: CreateMethod = CreateMethod.BIND
) = viewBinding(T::class.java, createMethod)
...
复制代码
能够看到这是一个扩展函数。第一个知识点来了!markdown
扩展函数定义:不改变原有类的状况下,扩展新的功能
。ide
首先肯定一点扩展函数针对的是类,为类提供新的功能。那是怎么实现的呢,看下面的示例:函数
我定义了一个 String
的扩展函数用以输出它的长度:post
private fun String.printLength() {
}
复制代码
转换成 java
代码看下性能
private final void printLength(String $this$printLength) {
}
复制代码
这样就很好的理解扩展函数的本质了:扩展函数的本质就是一个普通的函数,它不会对原有类作任何修改,不同的地方在于它默认以类对象做为函数的参数
。学习
在扩展函数内部你能够经过 this
关键字访问传过来的在点符号前的对象,也就是上面示例中的 $this$printLength
参数。而 this
也能够省略。优化
private fun String.printLength() {
Log.e("length", "$length")
}
复制代码
转换为 java
代码以下:this
private final void printLength(String $this$printLength) {
Log.e("length", String.valueOf($this$printLength.length()));
}
复制代码
val name = "张三"
name.printLength()
复制代码
输出为:2。
这也就是为何在 ViewBindingDelegate
项目中会忽然出现 activity
变量的缘由:
@JvmName("viewBindingActivity")
public fun <T : ViewBinding> ComponentActivity.viewBinding(
viewBindingClass: Class<T>,
rootViewProvider: (ComponentActivity) -> View
): ViewBindingProperty<ComponentActivity, T> {
return viewBinding { activity -> ViewBindingCache.getBind(viewBindingClass).bind(rootViewProvider(activity)) }
}
复制代码
扩展函数和普通函数的区别:
被扩展类型.函数名()
this
(可省略) 关键字访问被扩展的目标类对象。仍是上面的代码:
@JvmName("inflateViewBindingActivity")
public inline fun <reified T : ViewBinding> ComponentActivity.viewBinding(
createMethod: CreateMethod = CreateMethod.BIND
) = viewBinding(T::class.java, createMethod)
复制代码
发现两个不太认识的关键字:inline
、reified
。在介绍它们以前应该先理解两个概念:
高阶函数
:能够将函数用做参数或返回值的函数。内联函数
:使用 inline
修饰的函数。能够消除使用高阶函数时所带来的资源消耗。先看一个正常的函数,两数相加:
private fun addTwoNumbers(firstNumber: Int, secondNumber: Int): Int {
return firstNumber + secondNumber
}
复制代码
很简单,没有什么好说的。可是发现一个问题,若是计算两数相减、相乘、相除就须要再定义三个函数,可是并不想这么作,怎么办呢。这种状况下高阶函数就能够派上用场了,新函数以下:
private fun calculateTwoNumber(
firstNumber: Int,
secondNumber: Int,
calculate: (Int, Int) -> Int
): Int {
return calculate(firstNumber, secondNumber)
}
复制代码
函数 calculateTwoNumber
接受一个函数参数 calculate
,calculate
函数接受两个 Int
型参数并返回 Int
型结果。calculateTwoNumber
就是一个高阶函数。
转成 java
来看下:
private final int calculateTwoNumber(int firstNumber, int secondNumber, Function2 calculate) {
return ((Number)calculate.invoke(firstNumber, secondNumber)).intValue();
}
复制代码
发现 calculate
的参数类型是 Function2
,既然有了 Function2
那是否是还有 Function四、五、六、七、八、9
呢。这个能够有,事实上有 Function0~22
共 23
个接口类型。
public interface Function<out R>
public interface Function0<out R> : Function<R> {
public operator fun invoke(): R
}
public interface Function1<in P1, out R> : Function<R> {
public operator fun invoke(p1: P1): R
}
/** 接收两个参数的 Function */
public interface Function2<in P1, in P2, out R> : Function<R> {
/** 执行 invoke 函数 经过参数 P一、P2 获得并返回结果 R */
public operator fun invoke(p1: P1, p2: P2): R
}
...
public interface Function10<in P1, in P2, in P3, in P4, in P5, in P6, in P7, in P8, in P9, in P10, out R> : Function<R> {
public operator fun invoke(p1: P1, p2: P2, p3: P3, p4: P4, p5: P5, p6: P6, p7: P7, p8: P8, p9: P9, p10: P10): R
}
...
复制代码
它们的区别就是传参数量的区别。
那如今如今清楚了所谓的高阶函数其实编译成 java
就是 具备 Function 参数类型或返回值为 Function 类型的函数
。
kotlin
准备了两种方式能够得到 Function
对象:
lambda
表达式;lambda
表达式语法以下:
{参数声明 -> 函数体}
使用 lambda
注意如下几点:
{a: Int, b: Int -> a + b}
等价于
{a, b -> a + b}
复制代码
lambda
表达式能够放在圆括号以外。calculateTwoNumber(1, 2, {a: Int, b: Int -> a + b})
等价于
calculateTwoNumber(1, 2) { a: Int, b: Int -> a + b }
复制代码
lambda
表达式是调用时惟一的参数,那么圆括号能够彻底省略。run { println("...") }
复制代码
lambda
表达式只有一个参数时能够不用声明惟一的参数并忽略 ->
。val ints = listOf<Int>()
ints.filter { it > 0 }
复制代码
lambda
表达式默认返回最后一个表达是的值,也能够经过 return
显示指定返回值。ints.filter {
val shouldFilter = it > 0
shouldFilter
}
ints.filter {
val shouldFilter = it > 0
return@filter shouldFilter
}
复制代码
lambda
表达式中不可直接使用 return
,要退出 lambda
须要用到标签。可是若是传给的函数是内联(内联函数在下文讲解)的,能够直接使用 return
。fun ordinaryFunction(block: () -> Unit) {
println("hi!")
}
fun foo() {
ordinaryFunction {
return // 错误:不能使 `foo` 在此处返回
return@ordinaryFunction // 正确
}
}
fun main() {
foo()
}
复制代码
lambda
表达式语法缺乏指定函数的返回类型的能力。在大多数状况下返回类型能够自动推断出来。可是若是确实须要显式指定,那就须要用到 匿名函数
了。
匿名函数和常规函数的区别在于匿名函数没有函数名。其余和常规函数如出一辙。
fun(x: Int, y: Int): Int {
return x + y
}
复制代码
若是函数返回类型能够推导出来那么返回类型也能够省略。
如今已经清楚了高阶函数的定义,那咱们来用高阶函数来计算 0~10
的和:
var result = 0
for (i in 0..10) {
result = calculateTwoNumber(result, i) { a: Int, b: Int -> a + b }
}
Log.e("highfun", "$result") // 55
复制代码
完美!结果明显是正确的。那再看一下编译后的代码:
int result = 0;
int i = 0;
for(byte var4 = 10; i <= var4; ++i) {
result = this.calculateTwoNumber(result, i, (Function2)null.INSTANCE);
}
Log.e("highfun", String.valueOf(result));
复制代码
能够发现每次循环都会建立 Function
实例,这样在大量循环状况下会产生大量对象,影响内存,这明显得优化。优化方式有两种:
优化一:将 lambda
放到循环外定义。
val addCalculate = { a: Int, b: Int -> a + b }
var result = 0
for (i in 0..10) {
result = calculateTwoNumber(result, i, addCalculate)
}
复制代码
优化二:使用 inline
修饰高阶函数为内联函数。
private inline fun calculateTwoNumber(
firstNumber: Int,
secondNumber: Int,
calculate: (Int, Int) -> Int
): Int {
return calculate(firstNumber, secondNumber)
}
复制代码
使用 inline
修饰高阶函数后查看编码以后的代码:
for(byte var4 = 10; i <= var4; ++i) {
int $i$f$calculateTwoNumber = false;
int var9 = false;
result += i;
}
复制代码
能够发现 lambda
表达式的函数体被添加到了表达式被调用的地方,从而避免了建立 Function
对象。
注意:
内联虽然会提高性能,但同时也会致使生成的代码增长,因此应避免内联过大的函数
。
在上文中提到过传递给 inline
内联函数的 lambda
表达式中可使用 return
返回。那咱们来看下面的例子:
private fun callFunction() {
inlined {
Log.e("inline", "2")
return
}
}
private inline fun inlined(body: () -> Unit) {
Log.e("inline", "1")
body()
Log.e("inline", "3")
}
输出 ------
E/inline: 1
E/inline: 2
复制代码
能够看到代码中有三条日志打印信息,可是输出中只打印了两条。这里 return
在输出最后一条日志信息时直接结束了函数。因此使用 inline
内联函数时应该避免直接使用 return
,而改用 return@标签
的方式。修改下代码:
private fun callFunction() {
inlined {
Log.e("inline", "2")
return@inlined
}
}
输出 ------
E/inline: 1
E/inline: 2
E/inline: 3
复制代码
kotlin
中也提供了两个修饰符来帮助限制在 lambda
中直接使用 return
:
noinline
crossinline
noinline
若是但愿只内联一部分传给内联函数的 lambda
表达式参数,那么能够用 noinline
修饰符标记不但愿内联的函数参数:
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { …… }
复制代码
能够内联的 lambda
表达式只能在内联函数内部调用或者做为可内联的参数传递,可是 noinline
的能够以任何咱们喜欢的方式操做:赋值给变量
、传递给其余高阶函数
等等。
并且使用 noinline
修饰的函数参数,在为其传递 lambda
表达式时不能直接使用 return
否则会报错,须要使用 return@标签
。修改上面的示例:
private inline fun inlined(noinline body: () -> Unit) {
Log.e("inline", "1")
body()
Log.e("inline", "3")
}
输出 ------
E/inline: 1
E/inline: 2
E/inline: 3
复制代码
可是使用 noinline
也会出现一个问题,咱们看一下编译后的 java
代码:
private final void callFunction() {
Function0 body$iv = (Function0)null.INSTANCE;
int $i$f$inlined = false;
Log.e("inline", "1");
body$iv.invoke();
Log.e("inline", "3");
}
复制代码
看起来跟没有使用 inline
修饰的高阶函数调用是如出一辙的。并且你会看到警告信息:
Expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types
意思就是若是一个内联函数没有可内联的函数参数而且没有具体化的类型参数,那么这样的函数极可能并没有益处(若是你确认须要内联,则能够用@Suppress("NOTHING_TO_INLINE")
注解关掉该警告)。
那有没有便可以函数内联还能够保证 lanmbda
传参里没有直接使用 return
呢。
crossinline
crossinline
和 noinline
均可以限制 lambda
传参不可直接使用 return
,区别在于 crossinline
修饰的函数参数仍然是内联的。修改上面的示例:
private inline fun inlined(crossinline body: () -> Unit) {
Log.e("inline", "1")
body()
Log.e("inline", "3")
}
复制代码
查看编译后的 java
代码:
private final void callFunction() {
int $i$f$inlined = false;
Log.e("inline", "1");
int var3 = false;
Log.e("inline", "2");
Log.e("inline", "3");
}
复制代码
inline
内联函数还提供了另外一个有意思的能力:reified
。
reified
主要简化了访问类型参数的能力,看以下代码:
private inline fun <T: Activity> inlined(clazz: Class<T>) {
body()
Log.e("inline", "${clazz.name}")
}
调用:
inlined(MainActivity::class.java)
复制代码
其实没什么问你题,就是看起来不是很优雅(装X),那怎么办呢。使用 reified
改造一下:
private inline fun <reified T: Activity> inlined() {
body()
Log.e("inline", "${T::class.java.name}")
}
调用:
inlined<MainActivity>()
复制代码
查看编译后的 java
代码其实没什么差异,就是简便轻巧!
本节主要介绍了 kotlin
中的高阶函数和内联函数。高阶函数能够将函数用做参数或返回值,可是使用高阶函数会有必定的性能损耗,可使用 inline
修饰为内联函数以免性能损耗,而且为了不代码量会增长,因此应避免内联过大的函数。另外使用 noinline
、crossinline
修饰符能够限制 lambda
传参中直接使用 return
关键字以免影响函数正常执行。内联函数还提供了 reified
简化在函数中使用类型参数。
欢迎留言一块儿交流学习!