Kotlin之函数的默认参数

Kotlin之函数的默认参数

Android小彩虹2021-08-25 8:42:24270A+A-

前言

​ 在Java中,为函数的参数添加默认值是不被允许的,这是为了防止默认参数函数重载同时使用时二义性的问题,考虑下面的例子:

void func(p1: String, p2: String, p3: String = "default") {
	// do something
}

void func(String p1, String p2) {
	// do something
}

假设上面的代码是可以编译通过的,那么当调用func("p1","p2") 时,编译器会不知道到底该调用哪个方法。所以Java是不支持默认参数的,但是依然可以通过函数重载的方式实现默认参数的功能,这也是我们最普遍使用的方式:

void func(String p1, String p2, String p3) {
	// do something
}

void func(String p1, String p2) {
	func(a, b, "default");
}

通过上述函数重载的方式,也可以实现默认参数,但是有个问题也很明显,就是如果要支持默认参数,我们需要写很多的模版代码,好像也不是那么方便。然而,Kotlin 提供了默认参数的支持,接下来看看Kotlin中对于默认参数的支持是怎样的,又是怎么解决我们开始提到的那个二义性的问题的。

使用

在Kotlin中,使用默认参数也很简单,在函数定义中直接赋值即可:

fun func(p1: String, p2: String, p3: String = "default") {
	// do something
}

上述函数定义中,c 的默认值就是default,可以这样去调用 func("p1","p2")。同样的,针对构造函数,也可以指定默认值:

class TestDefaultParameters (
    val name: String,
    val type: String = "default"
){}

那么如果想要在Java中调用kotlin带有默认参数的函数怎么做呢?如果在Java中直接调用func("p1","p2")编译器会报错,这是需要给kotlin的方法加上Jvm重载的注解就可以了:

@JvmOverloads
fun func(p1: String, p2: String, p3: String = "default") {
	// do something
}

解析

接下来,我们看看Kotlin是如何实现默认参数的,首先,写一个例子如下:

fun main(args: Array<String>) {
    val testDefaultParameters = TestDefaultParameters("")
    testDefaultParameters.func("position1", "position2")
}

class TestDefaultParameters (
    val name: String,
    val type: String = "default"
){
    @JvmOverloads
    fun func(p1: String, p2: String, p3: String = "default") {
        // do something
    }

}

将上述的func的函数定义Decompile为Java实现:

@JvmOverloads
public final void func(@NotNull String p1, @NotNull String p2, @NotNull String p3) {
  Intrinsics.checkParameterIsNotNull(p1, "p1");
  Intrinsics.checkParameterIsNotNull(p2, "p2");
  Intrinsics.checkParameterIsNotNull(p3, "p3");
}

// $FF: synthetic method
public static void func$default(TestDefaultParameters var0, String var1, String var2, String var3, int var4, Object var5) {
  if ((var4 & 4) != 0) {
  	var3 = "default";
	}
	var0.func(var1, var2, var3);
}

@JvmOverloads
public final void func(@NotNull String p1, @NotNull String p2) {
	func$default(this, p1, p2, (String)null, 4, (Object)null);
}
...
// 调用func函数
TestDefaultParameters.func$default(testDefaultParameters, "position1", "position2", (String)null, 4, (Object)null);

中间一些代码我省略了,可以看到,Kotlin编译器为我们生成了三个func的重载方法,下面我们依次来看一下分别都是什么函数:

  • 首先看到的第一个函数是带有三个参数的func,函数内部都做了空安全的检查,这是kotlin的特性,由于声明函数时参数都是不为空的,所以这里需要检查参数是否为空,会抛出异常。
  • 第二个函数我们看到名字是func$default, 并不是func的方法重载,而是一个新的方法,这就是默认参数实现的关键方法,这里暂且按下不表,后面详细讲解。
  • 第三个函数依然是func方法的重载,可以看到这个方法只有两个参数,并且内部调用了第二个方法。其实这个方法是给Java调用的,由于我们将func函数声明为@JvmOverloads,所以当Java在不传递默认参数调用func的时候,实际上调用的是这个方法。如果将@JvmOverloads去掉的话,是没有这个方法的。

在了解了三个方法的作用之后,主要来看一下第二个方法:

// $FF: synthetic method
public static void func$default(TestDefaultParameters var0, String var1, String var2, String var3, int var4, Object var5) {
  if ((var4 & 4) != 0) {
  	var3 = "default";
	}
	var0.func(var1, var2, var3);
}

可以看到这个方法有6个参数,var0为Class对象,var1 ~var3 分别对应func函数的三个参数,然后有一个int类型的var4和一个Object类型的 var5var5这个大多数情况下都为null,默认参数实现的秘密主要是在这个var 4上, 来看看当调用函数使用默认参数时,是怎么调用的:

// kotlin
func("position1", "position2")
  
// Decompile
func$default(testDefaultParameters, "position1", "position2", (String)null, 4, (Object)null)

看到var4的值为4。是由于原函数是第三个参数为默认参数,即 position = 2位置的参数,所以 var4 = 2 2 2^2 =4

在看之前func$default 的方法实现:

if ((var4 & 4) != 0) {
  var3 = "default";
}

var4 & 4 != 0的时候,var3的值就等于默认参数。可以发现,func$default函数的int类型的参数就是表示第几个参数的值是默认参数的。下面看一个稍微复杂的例子:

fun func(p1: String = "position1", p2: String = "position2", p3: String = "position3") {
      // do something
}

// 调用
testDefaultParameters.func(p2 = "position2")

这次三个参数都有默认值,且调用时用具名参数指定p2的值为"position2"。下面看看Decompile后的代码:

// $FF: synthetic method
public static void func$default(TestDefaultParameters var0, String var1, String var2, String var3, int var4, Object var5) {
if ((var4 & 1) != 0) {
  var1 = "position1";
}

if ((var4 & 2) != 0) {
  var2 = "position2";
}

if ((var4 & 4) != 0) {
  var3 = "position3";
}

  var0.func(var1, var2, var3);
}

// 调用
func$default(testDefaultParameters, (String)null, "position2", (String)null, 5, (Object)null)

可以看到,这次方法体内有三个判断,因为有三个参数都是有默认值的,传递的参数为5,是由于函数调用时,index=0index=2的参数为默认参数,所以 var4 = 2 0 + 2 2 2^0 + 2^2 = 5。

这里大概解释一下为什么要这么设计的原因:

当写有多个条件,例如权限判断,index判断等逻辑的时候非常适合位运算。例如在上面的例子中,参数的index可以表示为:2的index幂的二进制数,例如 index = 0 即 2 0 2^0 , 用二进制表示为:0001,index = 1 即 2 1 2^1 ,表示为:0010(可以看作是二进制数中1的位置,即表示index)。

那么如果多个位置比如index=0与index=2呢?既可以表示为:0101。就是 2 0 + 2 2 = 5 2^0 + 2^2 = 5 。与目标所在的index进行按位与运算的时候,如果不等于0就表示该index符合条件。否则不符合。

回过头来看上述func$default函数体就清晰了,就是通过位置判断,当不是使用默认值的位置时,就不使用默认值。

上述例子中,我们看到含有默认值的参数的函数在Decompile之后,有一个始终为null的Object参数,而且也没有被使用到。那么这个参数有什么用呢?这个参数会在尝试重写有默认参数的函数时用到。例如下面的例子:

open class TestDefaultParameters {
    open fun func(p1: String = "position1", p2: String = "position2", p3: String = "position3") {
        // do something
    }
}

class TestDefaultChild : TestDefaultParameters() {
    override fun func(p1: String, p2: String, p3: String) {
        // do something
    }
}

将上述代码编译一下:

// $FF: synthetic method
public static void func$default(TestDefaultParameters var0, String var1, String var2, String var3, int var4, Object var5) {
    if (var5 != null) {
       throw new UnsupportedOperationException("Super calls with default arguments not supported in this target, function: func");
    } else {
       if ((var4 & 1) != 0) {
          var1 = "position1";
       }

       if ((var4 & 2) != 0) {
          var2 = "position2";
       }

       if ((var4 & 4) != 0) {
          var3 = "position3";
       }

       var0.func(var1, var2, var3);
}
}

可以看到,当调用超类使用默认参数在当前版本是不允许的(以后可能允许)。这就是Object参数的用处。

总结

以上就是对kotlin的默认参数实现的一些总结。

顺便打个广告,猿辅导研发招聘各岗位,有大量HC。感兴趣的话可以简历发我的邮箱哦:fb0122@163.com。欢迎各位大佬来撩~

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们