kotlin - 扩展函数、高阶函数、内联函数

关键词

  • 扩展函数
  • 高阶函数
  • 内联函数

在上篇文章 偷师 - 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)
复制代码

发现两个不太认识的关键字:inlinereified。在介绍它们以前应该先理解两个概念:

  • 高阶函数:能够将函数用做参数或返回值的函数。
  • 内联函数:使用 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 接受一个函数参数 calculatecalculate 函数接受两个 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~2223 个接口类型。

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 注意如下几点:

  1. 参数声明类型可选,也就是说能够不标注参数类型。
{a: Int, b: Int -> a + b}
等价于
{a, b -> a + b}
复制代码
  1. 若是高阶函数的最后一个参数是函数,那么做为相应参数传入的 lambda 表达式能够放在圆括号以外。
calculateTwoNumber(1, 2, {a: Int, b: Int -> a + b})
等价于
calculateTwoNumber(1, 2) { a: Int, b: Int -> a + b }
复制代码
  1. 若是该 lambda 表达式是调用时惟一的参数,那么圆括号能够彻底省略。
run { println("...") }
复制代码
  1. lambda 表达式只有一个参数时能够不用声明惟一的参数并忽略 ->
val ints = listOf<Int>()
ints.filter { it > 0 }
复制代码
  1. lambda 表达式默认返回最后一个表达是的值,也能够经过 return 显示指定返回值。
ints.filter {
    val shouldFilter = it > 0 
    shouldFilter
}

ints.filter {
    val shouldFilter = it > 0 
    return@filter shouldFilter
}
复制代码
  1. 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 对象。

注意:内联虽然会提高性能,但同时也会致使生成的代码增长,因此应避免内联过大的函数

noinline、crossinline

在上文中提到过传递给 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 crossinlinenoinline 均可以限制 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 修饰为内联函数以免性能损耗,而且为了不代码量会增长,因此应避免内联过大的函数。另外使用 noinlinecrossinline 修饰符能够限制 lambda 传参中直接使用 return 关键字以免影响函数正常执行。内联函数还提供了 reified 简化在函数中使用类型参数。

欢迎留言一块儿交流学习!

相关文章
相关标签/搜索