携手创作,共同成长!这是我参与「日新计划 8 月更文挑战」的第22天,点击查看活动详情

前言

Kotlin中的效果域函数是规范库中包括的几个常用函数,let、run、with、apply以及also。 从本篇起来介绍一下 Kotlin 中的效果域函数,分上下两篇。上篇会阐明几个常见效果域函数,剖析一下run函数,以及对比一下 Java 中没有效果域函数状况。

概览

Kotlin 作用域函数[Scope Function](上)

1. 常见的四个效果域函数

学习 Kotlin 肯定会碰到 run/let/apply/also 这四个函数,它们是Kotlin规范库中的几个常用函数,效果在目标上时,履行给定的block代码块。形成一个暂时效果域,在这个效果域中,能够拜访该目标而无需称号,也被称为效果域函数(scope functions)

下面表格展示常用几个效果域函数的对比,依据表格在事务场景选择合适效果域函数。

函数 目标引用 返回值 是否为扩展函数
let it Lambda表达式成果
run this Lambda表达式成果
apply this 上下文目标
also it 上下文目标

依据预期目的选择合适效果域函数的指南:

  • 对一个非空(non-null)目标履行 lambda 表达式:let
  • 将表达式作为变量引进为局部效果域中:let
  • 目标装备:apply
  • 目标装备而且计算成果:run
  • 在需求表达式的当地运行句子:非扩展的run
  • 附加效果:also

2. run 办法运用

在项目中,有以下一段代码:

public class PlayManager {
    /** 初始值为空,需在资源初始化之后再拿到目标 */
    private Player player = null;
    /** 播映音乐 */
    public void play(String path) {
        if (player != null) {
            player.init(path);
            player.prepare();
            player.start();
        }
    }
}

Kotlin等效代码为:

public class PlayManager {
    /** 初始值为空,需在资源初始化之后再拿到目标 */
    private var player: Player? = null
    /** 播映 */
    fun play(path: String) {
        player?.init(path)
        player?.prepare()
        player?.start()
    }
}

运用 Kotlin 的 run 办法:

public class PlayManager {
    /** 初始值为空,需求在资源初始化之后再拿到目标 */
    private var player: Player? = null
    /** 播映 */
    fun play(path: String) {
        player?.run {  // 目标调用run
            init(path)
            prepare()
            start()
        }
    }
}

run 调用是一种函数调用的特别写法,即当 lambda 作为函数的最后一个参数时,能够写在函数括号外部,也就是说object.run { }object.run({ })是等价的。这种代码写起来看起来都更简洁。

run的功能很简单,主要做两件事:

  1. 把 lambda 内部的 this 改成了对应调用目标;
  2. run 函数会返回 lambda表达式的返回值。

run 办法到达以下三个效果:

  1. this 的变化,不再需求重复的输入变量,和链式调用殊途同归;

  2. 把可空目标转换为了非空目标,由于run办法是?.调用,player 不为空才会履行。考虑到并发,Kotlin 要求每次调用可空特点时要进行判空。运用 run 办法等效于先把可空特点用暂时变量持有再运用,这样就消除了并发竞争的影响。

  3. 在一个函数里声明的这个一个小“代码块“,表示和其他无关的代码隔离,完成了函数内部的高内聚。能够添加代码的可读性,让人一看就理解:“这是针对此目标的一系列操作,函数里关于此目标的运用只需求重视这个代码块即可”。

第3点是非常棒的,这样不仅是进步开发效率,更是引导开发者写出好保护的代码。在写 Java代码时,很简单不自觉的写出某个目标在函数头操作一下,隔几行调用一下,隔几行又操作一下的代码。阅读者很简单误以为这些代码之间有着次序上的耦合,从而继续按照这个“隐含的规矩“来保护代码。却不知其时的开发者只是想到哪写到哪,实际并不存在这样的隐含联系。运用 run能够在函数内部快速建立起一个个代码块,让函数拥有更明晰的结构,又不用花费很大精力保护代码逻辑。

3. run 函数代码剖析

run 源码如下:

@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

从上面函数源码看,触及的根本都是编译器相关的。包括了泛型,inline,类扩展 lambda(T.() -> R),contract 4 个概念。

inline,表示内联函数,在编译期调用这个函数的当地会被替换为函数包括的代码。

inline 的优点是调用该办法不再有调用办法的功能消耗,即不会跳转和发生栈帧;弊端是可能会使二进制文件体积增大,尤其是函数代码量大的时候。所以 inline 适合用在代码量小的函数,run 就很契合这个条件。能够得出结论:编译器编译时会把 inline 函数内联到实际调用方位,所以运用 run 办法时不会有办法调用的功能损耗。

@kotlin.internal.InlineOnly,实际效果为对 Java 不行见(private),由于 Java 不支持 inline。对 Java 不行见后,这个 inline 办法则能够不在字节码里存在,由于调用的当地全部都内联了。

Java 虽没有内联函数,但 JVM 是有内联优化的,只是这个优化无法精确控制。

类扩展 lambda(关键字 lambda with class extension),即入参的声明 T.() -> R。扩展 lambda 能够理解为给类扩展一个 lambda 函数。它的效果和扩展办法一样,在 扩展 lambda 效果域内,以目标作为 this 来操作这个目标。

contract 契约,指的是代码和 Kotlin 编译器的契约。举一个比如,对局部变量添加了假如为空则 return 的逻辑,Kotlin 编译器便能够智能的辨认出 return 之后的局部变量必定不为空,局部变量的类型会退化为非空类型。但假如把是否为空的代码封装进一个扩展办法如 Any?.isNotNull() 里,那么编译器就无法辨认 return 后边的代码局部变量是否为空,事实上这个局部变量依然是可空类型。

这儿能够声明一个 contract,告知编译器假如Any?.isNotNull() 返回了 true,则表示目标非空。这样在代码里履行了 isNotNull() 办法之后,return 后边的代码,局部变量也能正确退化为非空类型。具体比如咱们能够看官方 Collections.kt 的 Collection<T>.isNullOrEmpty()

4. Java 没有效果域函数

效果域函数需求类扩展内联这两个特点,才能最大化表现其价值。没有类扩展,this 的切换需求经过继承或者匿名类来完成,做不到通用

let 这种不需求切换 this 的效果域函数,由于没有类扩展能力而为了寻求通用性,也只能经过静态工具类来完成,效果是打折扣的。

Java是没有内联的,虽有 JVM 内联优化支撑,但内联优化只对 final 且调用次数数量级较大的办法有效。假如像 Kotlin 这样规模化的运用效果域函数,对功能是有不行忽视的影响的。

在(JUEJIN)一同共享知识,Keep Learning!