Kotlin学习教程之函数的默认参数
fb0122 人气:0前言
在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类型的 var5。var5这个大多数情况下都为null,默认参数实现的秘密主要是在这个var 4上, 来看看当调用函数使用默认参数时,是怎么调用的:
// kotlin func("position1", "position2") // Decompile func$default(testDefaultParameters, "position1", "position2", (String)null, 4, (Object)null)
看到var4的值为4。是由于原函数是第三个参数为默认参数,即 position = 2位置的参数,所以 var4 = 222^222=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=0 和 index=2的参数为默认参数,所以 var4 = 20+222^0 + 2^220+22 = 5。
这里大概解释一下为什么要这么设计的原因:
当写有多个条件,例如权限判断,index判断等逻辑的时候非常适合位运算。例如在上面的例子中,参数的index可以表示为:2的index幂的二进制数,例如 index = 0 即 202^020 , 用二进制表示为:0001,index = 1 即 212^121 ,表示为:0010(可以看作是二进制数中1的位置,即表示index)。
那么如果多个位置比如index=0与index=2呢?既可以表示为:0101。就是 20+22=52^0 + 2^2 = 520+22=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的默认参数实现的一些总结。
加载全部内容