Kotlin标准库函数源码解析

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 关键字实际上增加了代码量,但是提升了性能,而且增加的代码量是在编译期执行的,对程序可读性不会造成影响。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342