探究「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的某个语法糖感到好奇,不妨亲自揭开他的糖衣,探究其背后的故事~

相关推荐
每次的天空11 分钟前
kotlin与MVVM的结合使用总结(二)
android·开发语言·kotlin
李艺为32 分钟前
Android Studio执行Run操作报Couldn‘t terminate previous instance of app错误
android·ide·android studio
苏婳6664 小时前
雷电模拟器连接Android Studio步骤
android·android studio
tangweiguo030519875 小时前
Android TTL 串口通信工具类封装
android·java
明天就是Friday6 小时前
流水线(Pipeline)
android·流水线(pipeline)
海伟6 小时前
Missing classes detected while running R8报错解决方案
android
二流小码农6 小时前
鸿蒙开发:了解应用级配置信息
android·ios·harmonyos
weixin_4664851116 小时前
qt5中使用中文报错error: C2001: 常量中有换行符
android·java·qt
苏柘_level616 小时前
【Android14】Android系统内置第三方应用时OpenSSL版本冲突的深度解析与解决方案优化
android
wd 6716 小时前
sql注入拿shell
android·数据库·sql