探究「Kotlin语法糖」背后的本质

Kotlin 是一种现代但已经成熟的编程语言,旨在让开发人员更快乐。它简洁、安全、可与 Java 和其他语言互操作。 ------ Kotlin官网

Kotlin是一门年轻的语言,由世界上IDE做的最好的Jetbrains 公司在2010年面向公众推出,直到2017年Google在I/O大会上宣布推荐Kotlin作为Android开发语言才进入Android开发者视野 ,2019年的I/O大会上,Google再度加码,宣布Kotlin为Android开发的首选语言,并且Android官方的类库代码将逐渐切换为Kotlin实现,至此确立Kotlin语言在Android开发中的地位。

正如官方文档说的那样,Kotlin语法带有许多方便高效的语法设计,但又与 Java 完美兼容。有人说这些只不过是Kotlin的 「语法糖」 ,本质还是JVM那一套,本篇文章旨在剥开这层糖衣,探究 Kotlin 的一些语法特性在编译后的本质。

函数的作用域

顶级函数 top-level functions

在Java中,任何函数与变量都需要声明在一个类中,Kotlin摒弃了这个设定,允许开发者脱离类和接口定义函数与变量,Kotlin称之为 顶级函数。 我们尝试在Android Studio中声明一个顶级函数来探究本质:

kotlin 复制代码
package com.example

fun topFunction(): String {
    return "这是一个顶级方法"
}

借助 IDE 提供的一些能力,我们可以很方便的看到这段Kotlin代码等价的Java代码,具体操作路径如下: 上面这段代码对应的Java代码如下:

java 复制代码
package com.example;

public final class TopLevelDemoKt {
    @NotNull
    public static final String topFunction() {
        return "这是一个顶级方法";
    }
}

可以看到,所谓的脱离类限制的顶级函数,实际上在Java中其实还是处于一个类中,这个类的名字由Kotlin文件名 + Kt组成,对应的方法变成了Java中的静态方法。于是我们在Java代码中可以这样调用上面那段Kotlin的顶级函数:

java 复制代码
public class UsingTopLevelFunctionInJava {
    public static void main(String[] args) {
        String text = TopLevelDemoKt.topFunction();
    }
}

我们还也可以通过注解@file:JvmName指定生成的类名:

kotlin 复制代码
@file:JvmName("Top")
package com.example

fun topFunction(): String {
    return "这是一个顶级方法"
}

对应的Java代码调用也就变成了Top.topFunction()

本地函数 local functions

下面展示一段由Kotlin编写的模拟校验用户注册提供的账号密码是否合规的代码,我们假设用户账户或者密码长度小于7为不合规,直接抛出异常:

kotlin 复制代码
fun registrationCheck(username: String, password: String): Boolean {
    var variableOutsideTheLocalFunction = 1
    fun validateInput(input: String){
        // 本地方法可以引用到方法外的变量
        variableOutsideTheLocalFunction++
        if (input.length < 7) {
            throw IllegalArgumentException("The length must be greater than 7")
        }
    }
    validateInput(username)
    validateInput(password)
    return true
}

和我们认知中的Java代码不同,该代码的validateInput函数写在了registrationCheck方法体里,这种"函数的函数"的形式,Kotlin官方称之为本地函数Local Functions。 可以看到的是,本地函数可以访问外部函数的局部变量,在上面的例子中,variableOutsideTheLocalFunction定义在本地方法之外,但仍旧可以在本地方法内引用到。 这又是什么magic呢?我们来看与上面代码等价的Java代码:

java 复制代码
public final class KotlinDemoKt {
    public static final boolean registrationCheck(@NotNull String username, @NotNull String password) {
        Intrinsics.checkNotNullParameter(username, "username");
        Intrinsics.checkNotNullParameter(password, "password");
        final Ref.IntRef variableOutsideTheLocalFunction = new Ref.IntRef();
        variableOutsideTheLocalFunction.element = 1;
        <undefinedtype> $fun$validateInput$1 = new Function1() {
        	// $FF: synthetic method
        	// $FF: bridge method
        	public Object invoke(Object var1) {
        		this.invoke((String)var1);
        		return Unit.INSTANCE;
    		}

   			public final void invoke(@NotNull String input) {
        		Intrinsics.checkNotNullParameter(input, "input");
        		int var10001 = variableOutsideTheLocalFunction.element++;
        		if (input.length() < 7) {
            		throw (Throwable)(new IllegalArgumentException("The length must be greater than 7"));
        		}
    		}
		};
		$fun$validateInput$1.invoke(username);
		$fun$validateInput$1.invoke(password);
		return true;
	}
}

代码相对来说还是比较好理解的,关键就是我们在Kotlin中定义的本地函数validateInput在Java中被声明成了一个Function1的实现,并通过方法invoke进行调用,我们一起来看看这个Funtion1究竟是何方神圣:

java 复制代码
/** A function that takes 0 arguments. */
public interface Function0<out R> : Function<R> {
    /** Invokes the function. */
    public operator fun invoke(): R
}
/** A function that takes 1 argument. */
public interface Function1<in P1, out R> : Function<R> {
    /** Invokes the function with the specified argument. */
    public operator fun invoke(p1: P1): R
}

/** 省略 N 个 相似代码... */


/** A function that takes 22 arguments. */
public interface Function22<in P1, in P2, in P3, in P4, in P5, in P6, in P7, in P8, in P9, in P10, in P11, in P12, in P13, in P14, in P15, in P16, in P17, in P18, in P19, in P20, in P21, in P22, out R> : Function<R> {
    /** Invokes the function with the specified arguments. */
    public operator fun invoke(p1: P1, p2: P2, p3: P3, p4: P4, p5: P5, p6: P6, p7: P7, p8: P8, p9: P9, p10: P10, p11: P11, p12: P12, p13: P13, p14: P14, p15: P15, p16: P16, p17: P17, p18: P18, p19: P19, p20: P20, p21: P21, p22: P22): R
}

进入kotlin.jvm.functions.Functions.kt文件下面,我们能看到这里预定义了很多类似的接口,实际上,Lambda 表达式就是实现了这些接口之一的类。

Kotlin 通过本地函数为开发者提供了一种比private更小的、更容易理解和维护的方法块,通过这种方式大大提高代码可读性和可复用性。

同时,我们应该注意到由于其特有的实现方式(匿名内部类),相比于直接使用私有函数,使用本地函数存在额外的性能开销,虽然这点开销微乎其微,但仍然需要开发者进行一定的取舍。

拓展

Kotlin支持为已经声明好的类扩展新的方法和新的属性。

kotlin 复制代码
/**
 * androidx.core.view.View.Kt中为View的扩展的visible属性
 */
public inline var View.isVisible: Boolean
	get() = visibility == View.VISIBLE
	set(value) {
    	visibility = if (value) View.VISIBLE else View.GONE
	}

/**
 * androidx.core.view.View.Kt中为View的扩展的用于更新padding的方法
 */
public inline fun View.setPadding(@Px size: Int) {
    setPadding(size, size, size, size)
}

等价的Java代码大概是这样的:

java 复制代码
public static final boolean isVisible(@NotNull View $this$isVisible) {
    int $i$f$isVisible = 0;
    Intrinsics.checkNotNullParameter($this$isVisible, "$this$isVisible");
    return $this$isVisible.getVisibility() == 0;
}

public static final void setVisible(@NotNull View $this$isVisible, boolean value) {
	int $i$f$setVisible = 0;
	Intrinsics.checkNotNullParameter($this$isVisible, "$this$isVisible");
	$this$isVisible.setVisibility(value ? 0 : 8);
}

public static final void setPadding(@NotNull View $this$setPadding, @Px int size) {
    int $i$f$setPadding = 0;
    Intrinsics.checkNotNullParameter($this$setPadding, "$this$setPadding");
    $this$setPadding.setPadding(size, size, size, size);
}

可以看到,在等价的Java代码中,拓展函数的第一个参数是被拓展类本身,第二个参数才是拓展函数中定义的参数,同样,拓展属性也是通过拓展对应的getter / setter方法来实现的。

拓展的出现,可以让我们无需修改类的源代码就能向现有的类中添加新的功能,降低了修改代码带来的风险,同时也使得代码的可读性更好。Android官方也在androidx加入了许多好用的拓展属性和拓展方法。

默认参数

使用Java代码编写Android中的自定义View大概是这样式儿的:

java 复制代码
public class MViewWithJava extends View {
    public MViewWithJava(Context context) {
        this(context, null);
    }

    public MViewWithJava(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MViewWithJava(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public MViewWithJava(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        // 省略相关业务逻辑...
    }
}

一行业务相关代码没写,光是构造函数声明了4个,最终都调用到了4个参数的构造函数中。这段代码如果使用Kotlin写呢?

java 复制代码
class MViewWithKotlin @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0,
) : View(context, attrs, defStyleAttr, defStyleRes) {
    
    init {
        // 省略相关业务逻辑代码
    }
}

可以看到得益于默认参数,代码明显简洁清晰了许多,这又是什么magic呢?我们来实验一下:

kotlin 复制代码
fun testDefaultArgs(p1: Int, p2: String = "test"){
    print("$p1$p2")
}

其对应的等价Java代码如下:

kotlin 复制代码
public static final void testDefaultArgs(int p1, @NotNull String p2) {
      Intrinsics.checkNotNullParameter(p2, "p2");
      String var2 = p1 + p2;
      System.out.print(var2);
   }

   // $FF: synthetic method
   public static void testDefaultArgs$default(int var0, String var1, int var2, Object var3) {
      if ((var2 & 2) != 0) {
         var1 = "test";
      }

      testDefaultArgs(var0, var1);
   }

可以看到,除了正常的testDefaultArgs方法外,等价的Java代码中还多出了一个testDefaultArgs$default方法,看方法名也知道这是实现默认参数的关键,该方法中除了我们声明的一个int参数和一个String参数外,还多出了一个int类型的参数和一个Object类型的参数。这个方法中的代码也很好理解,var2作为标记位,由编译器根据实际的代码进行赋值传参,var2 & 2中的2表示是第几位参数有默认参数,与var2做一些逻辑逻辑与操作判断对应的参数是否传递,若未传递就进行默认值赋值操作。

总结

Kotlin中语法的特性除了上述总结的顶级函数、本地函数、拓展属性和方法以及默认参数外,还有很多,比如解构函数、智能转换以及空安全等等等等,受限于篇幅原因不可能一一探索其背后的实现。本文旨在抛砖引玉,如果你也对Kotlin的某个语法糖感到好奇,不妨亲自揭开他的糖衣,探究其背后的故事~

相关推荐
找藉口是失败者的习惯3 小时前
Jetpack Compose 如何布局解析
android·xml·ui
Estar.Lee7 小时前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
温辉_xh8 小时前
uiautomator案例
android
工业甲酰苯胺9 小时前
MySQL 主从复制之多线程复制
android·mysql·adb
少说多做3439 小时前
Android 不同情况下使用 runOnUiThread
android·java
Estar.Lee11 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
找藉口是失败者的习惯11 小时前
从传统到未来:Android XML布局 与 Jetpack Compose的全面对比
android·xml
Jinkey12 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
大白要努力!14 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟15 小时前
Android音频采集
android·音视频