Kotlin第九讲---特有函数

内容简介

上一篇我了解到了 `Kotlin` 很重要的一个角色 `Lambda` 表达式,并且了解到了它的本质。接下来我们来看下 `Kotlin` 为我们特有类型的函数,方便我们开发。

包级函数

Kotlin 创建文件都是创建 .kt 文件,而 Java 创建的是 .java 文件。在 Java 文件中只能定义一个类(当且不考虑内部类),并且名字要和文件名相同,文件路径对应的就是包名,这些都是乌龟的腚(规定)。

Kotlin 随和多了,定义的 kt 文件不再是类的约束。 kt 文件只是一个编写代码的容器,我们可以随意定义多个类,你要是喜欢可以把整个程序都编写在一个 kt 文件中,并且包名也不再是必须是文件路径了。最重要的是我们可以直接在 kt 文件定义方法和变量,我们称为 包级函数丶 包级变量

我先定义了一个 Kot.kt 文件,对应包名 com.qihoo.Kot(也就是路径名)

接下来我修改了包名 com.qihoo.test 编译器没报错,并且直接定义了 main 丶 call 方法,以及 2 个变量。

/**
 * 修改包名
 */
package com.qihoo.test
/**
 * 直接定义 name 变量
 */
val name = "阿文"
val age = 18
/**
 * 在定义一个方法
 */
fun call(name: String, age: Int) {
    println("$name:$name")
}
/**
 * 直接定义类
 */
data classProgrammer(val name: String, val age: Int)
/**
 * main方法直接卸载 kt 文件了
 */
fun main() {
Programmer(name, age)
    call(name, age)
}

我们来看下产物,看看生成的 class 是什么?生成了一个 KotKt.class 文件,并且路径是 com/qihoo/test,是我们修改包名路径和 kt 文件路径无关。

这里我遇到过一个坑,做 A 项目的时候,拷贝了自己 B 项目的 kt 文件,忘记修改包名了。在混淆的时候配置的混淆配置不对,导致出了一些问题。所以在写 Kotlin 的时候,一定要记住 kt 文件的路径,并不是生成类的路径。

包级函数本质

我们来看看生成的 class 文件的源码,看看到底是产物是什么(其实大家猜也能猜到)?包级函数&变量存在于对应 文件名.class 类中的静态变量&方法

publicfinalclassKotKt{
privatestaticfinalint age = age;
@NotNull
privatestaticfinalString name = name;
@NotNull
publicstaticfinalString getName() {
return name;
}
publicstaticfinalint getAge() {
return age;
}
publicstaticfinalvoid call(@NotNullString name2, int age2) {
Intrinsics.checkParameterIsNotNull(name2, "name");
System.out.println(name2 + ':'+ name2);
}
publicstaticfinalvoid main() {
newProgrammer(name, age);
        call(name, age);
}
}

Java如何调用

看到了生成的类,我们应该知道了 java 如何调用这些方法和变量了。通过 KotKt 调用静态方法即可。

publicclassDemo{
publicstaticvoid main(String[] args) {
String name = KotKt.getName();
int age = KotKt.getAge();
KotKt.call(name, age);
}
}

我们 Java 调用这些包级函数,还有编写 文件名Kt 来进行调用,很不直观, Kotlin 为我们提供了 @file:JvmName("生成的类名") 注解来修改生成的类名(注意混淆哦),直接在 kt 文件中编写即可。

后续会有专门的一篇,讲解 Kotlin 为我们提供的几个注解,来方便我们与 Java 之间的互相调用。

Kotlin调用

相比较 Kotlin 调用就比较直观了,直接导包调用即可。我们在创建一个 kt 文件,尝试调用。是不是超级简单呀?

/**
 * 导入用的变量丶方法的包
 */
import com.qihoo.test.age
import com.qihoo.test.call
import com.qihoo.test.name
fun main() {
/**
     * 直接调用
     */
    call(name, age)
}

这补充一个知识点,前面应该也说过。我们知道包级函数&变量的调用都是通过导包的,这样就会有一个奇异,如果 2 个不同包的 2 个 kt 文件中,定义了 2 个同样的包级函数怎么办呢?

例如:

com.qihoo.kot.Kot.kt 文件

package com.qihoo.kot
/**
 * 定义 call 方法
 */
fun call() {
    println("我是在 com.qihoo.kot.kot.kt 文件定义的哦")
}

com.qihoo.kot2.Kot.kt 文件

package com.qihoo.kot2
/**
 * 定义 call 方法
 */
fun call() {
    println("我是在 com.qihoo.kot2.kot.kt 文件定义的哦")
}

我们尝试的调用,会报错哦。因为编译器不知道要调用那个 call 方法。这时候我们可以写全路径的方式调用,也可以通过 as 关键词重新命名,来解决调用歧义。

package com.qihoo.core
/**
 * 导入2个 call 方法,并通过 as 关键词,重新定义调用昵称
 *
 * 思考 as 关键词还在哪里用过呢?
 */
import com.qihoo.kot.call as kotCall
import com.qihoo.kot2.call as kot2Call
fun main() {
// 直接使用 重新定义调用即可
    kotCall()
    kot2Call()
}


扩展函数

不得不说 Kotlin 的扩展函数和 Lambda 简直将 Kotlin 推向了高潮。它给使用 Kotlin 的开发者们增加了无限的想象。

曾经我们写了无数的工具类,一般都是抽取静态方法。这种形式比较恶心,并且在协同开发过程,经常出现重复造轮子的工具类。而 Kotlin 的扩展方法,能优化一部分这样的问题(后续讲到的 Kotlin 的高阶函数,大部分都是通过扩展方法实现的)。

例如:我们经常数组有一个交换操作(以前是通过抽取工具类静态方法形式),接下来我们定义一个扩展函数来实现。需要对什么类进行扩展,只需要使用 类名.扩展函数昵称(参数1:类型,参数2:类型)

/**
 * 定义扩展扩展方法swap,我们是定义在了Array扩展上
 */
fun <T> Array<T>.swap(v1: Int, v2: Int) {
    val tmp = this[v1]
this[v2] = this[v1]
this[v1] = tmp
}
fun main() {
    val arrays = arrayOf("1", "2", "3")
/**
     * 可以发现 直接有 swap 方法了(编辑器会为我们提示此方法哦)
     */
    arrays.swap(0, 1)
}

扩展函数本质

是不是很神奇?扩展函数的函数体,既然能直接调用扩展类的变量和方法,难道编译期把扩展方法插入到对应的扩展类里面了?仔细想想也不可能,如果我为 Activity 扩展方法,你怎么可能注入到 Activity 类中,这些类都是在 ROM 中的。那他到底怎么实现的呢?我们来看下编译结果。

其实也很简单,就是多生成了一个静态方法,对应的第一个参数就是我们扩展类的调用对象罢了。从本质我们就能看出一个问题,扩展函数的函数体只能调用访问扩展类的公开方法和属性。现在知道为啥 kotlin 定义的变量和函数都是默认公开的了吧。

publicfinalclassCallKt{
publicstaticfinal<T> void swap(@NotNull T[] $receiver, int v1, int v2) {
Object tmp = $receiver[v1];
        $receiver[v2] = $receiver[v1];
        $receiver[v1] = tmp;
}
publicstaticfinalvoid main() {
        swap(newString[]{"1", "2", "3"}, 0, 1);
}
}

Kotlin 为我们封装的方法几乎都是以扩展函数的形式提供。文件读写,数组增删改查丶遍历都是以扩展函数的形式增加,大家可以创建一个对象,在通过代码提示看看有多少扩展函数吧。

内联函数

在讲解内联函数之前,大家可以了解下 Java 虚拟机的线程是如何调用方法的,线程执行完毕的依据又是什么呢(后面的扩展内容会讲到)?

我们如何定义内联函数呢?其实很简单只需要通过 inline 修饰方法即可,接下来我们来看看通过 inline 修饰后和未修饰后的区别吧。

/**
 * 未经过inline修饰的fun方法
 */
fun call(block: () -> Unit) {
    println("1")
    block()
    println("2")
}
/**
 * 调用执行
 */
fun main() {
    call {
        println("3")
}
}

看下编译的 Java 信息,我们可以看到 main 方法单纯的就是调用了 call 方法。

publicfinalclassKotlinInlineKt{
publicstaticfinalvoid main() {
// 1. 可以看到创建了一个Function0的对象,并且调用了call方法
      call((Function0)null.INSTANCE);
}
// $FF: synthetic method
publicstaticvoid main(String[] var0) {
      main();
}
// 2.call方法的定义没啥好说的
publicstaticfinalvoid call(@NotNullFunction0 block) {
Intrinsics.checkParameterIsNotNull(block, "block");
String var1 = "1";
boolean var2 = false;
System.out.println(var1);
      block.invoke();
      var1 = "2";
      var2 = false;
System.out.println(var1);
}
}

接下来我们尝试通过 inline 修饰方法的结果( Koltin 代码就不贴出来了,就是 call 方法多了个 inline 修饰符)。我们可以发现 main 方法没有直接调用 call 方法了,而是将 call 方法的函数复制了一份,直接写在 main 方法中了。试想下这样的坏处,就是单独的 class 文件的体积会变大(怪不得 Kotlin 的项目体积明显比 Java 的大)。

publicfinalclassKotlinInlineKt{
// 1. 看main方法都不调用call方法了和创建lambda对应的FunctionN对象了,直接将call方法的代码拷贝了一份(看上去打破了java的方法抽取服用的概念)
publicstaticfinalvoid main() {
int $i$f$call = false;
String var1 = "1";
boolean var2 = false;
System.out.println(var1);
int var3 = false;
String var4 = "3";
boolean var5 = false;
System.out.println(var4);
      var1 = "2";
      var2 = false;
System.out.println(var1);
}
// $FF: synthetic method
publicstaticvoid main(String[] var0) {
      main();
}
// 2.call方法的定义没啥好说的
publicstaticfinalvoid call(@NotNullFunction0 block) {
int $i$f$call = 0;
Intrinsics.checkParameterIsNotNull(block, "block");
String var2 = "1";
boolean var3 = false;
System.out.println(var2);
      block.invoke();
      var2 = "2";
      var3 = false;
System.out.println(var2);
}
}

这里有个点不知道大家有没有注意,通过 inline 修饰的方法,若行参中有 Lambda 表达式,也会默认将 Lambda 代码平铺到调用者函数中。试想下这样的好处是啥?在上一篇中我们学习过 Lambda 表达式的本质,会多生成一个 FunctionX 的类,如果这样做是不是就会少生成 1 个类呀?当然若我不想让 Lambda 也被内联也是可以的,只需要通过 noinline 修饰 Lambda 表达式。

/**
 * 经过inline修饰的fun方法
 * 通过 noinline 关键词修饰lambda表达式,表示这个表达式在 inline 函数中, lambda 不会被平铺代码.最终还是会以 Function 对象的形式调用
 */
inline fun call(noinline block: () -> Unit) {
    println("1")
/**
     * 此 lambda 并不会平铺到代码中去
     */
    block()
    println("3")
}

内联函数return问题

不知道大家有没有考虑,内联函数中编写 return 会中断调用函数吗?会不会将内联函数的 return 代码编译到调用者函数中呢? Kotlin 开发者可能是为了防止歧义,内联函数的 return 是不会中断调用函数的。但是默认内联函数的 Lambda 表达式的 return 会中断。

/**
 * 经过inline修饰的fun方法
 */
inline fun call(block: () -> Unit) {
    println("1")
    block()
    println("3")
}
/**
 * 验证下结论
 */
fun main() {
/**
     * 调用内联函数
     */
    call {
        println("测试结果")
return
}
}
输出结果:
1
测试结果

可以看到代码中没有输出 3, call 方法就结束了。查看下编译源码,发现的确没有将输出 3 的语句写入。

publicfinalclassCallKt{
publicstaticfinalvoid call(@NotNullFunction0<Unit> block) {
Intrinsics.checkParameterIsNotNull(block, "block");
System.out.println("1");
        block.invoke();
System.out.println("3");
}
publicstaticfinalvoid main() {
System.out.println("1");
System.out.println("测试结果");
// 没有将输出 3 的语句写入
}
}

接下来就有个问题,那如果我不想内联函数的 Lambda 中断函数呢?可通过 crossinline 修饰 Lambda 表达式。然后你会发现,若在 Lambda 表达式中写 return 会直接报错。

/**
 * 经过inline修饰的fun方法
 */
inline fun call(crossinline block: () -> Unit) {
    println("1")
    block()
    println("3")
}
/**
 * 验证下结论
 */
fun main() {
/**
     * 调用内联函数
     */
    call {
        println("测试结果")
// 这句代码会报错
return
}
}

其实这里有必要说下, Lambda 表达式本身是不允许写 return 的。只有在内联函数且没有通过 crossinline 修饰的 Lambda 才可以调用 return (可以直接中断调用函数)。那问题又来了,若我们的 Lambda 要有根据条件 return 的功能呢?只能通过 ifelse吗?其实我们只需要使用 return@函数昵称 即可。

/**
 * 经过inline修饰的fun方法
 */
fun call(block: () -> Unit) {
    println("1")
    block()
    println("3")
}
/**
 * 验证下结论
 */
fun main() {
/**
     * 调用内联函数
     */
    call {
if(Math.random() > 0.5) {
// 注意这里
return@call
}
        println("测试结果")
}
}

大家可以看下编译结果,还是很有意思的哦(不得不说 Kotlin 的编译器挺智能)。 Kotlin 提供给的很多扩展函数都是内敛函数,主要目的就是减少 Lambda 表达式生成的多余类。

扩展内容

以下内容都是扩展内容,懒得看可无视。

为何要有内联方法呢?他给我们带来了什么好处呢?我们来看下 Java 虚拟机是如何调用方法的吧!

栈帧概念:

这里要特别解释下栈帧,每个线程都会分配一个栈。这个栈中存放的数据就是栈帧,每个栈帧就代表的一个方法。

举个例子:当一个线程调用 a 方法, a 方法调用了 b 方法,那么这个线程的栈中就存放了 2 个栈帧,先将 a 方法压入栈,然后将 b 方法压入栈, b 方法执行完成弹出 b, a 方法执行完弹出 a 。当这个栈为空了,这个线程也就算是执行完成了。

看到这里我想大家知道为啥要有内联函数了吧?它可以带来几个好处的。

  1. 减少线程调用栈的栈入栈出、

  2. 减少 Lambda 生成不必要的多余 FunctionX 类

推荐阅读

--END--

识别二维码,关注我们

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值