本文最后更新于 172 天前,其中的信息可能已经有所发展或是发生改变。
有关 Kotlin 具名参数形参传参顺序导致输出结果发生改变问题的一些探索
具名参数
众所周知,Kotlin 拥有一种叫做具名参数(Named arguments)的特性,它在需要跳过可选参数,或是调整参数顺序的地方十分有效。
例如如下拥有五个参数,且后四个参数为可选参数的函数:
代码语言:javascript复制fun reformat(
str: String,
normalizeCase: Boolean = true,
upperCaseFirstLetter: Boolean = true,
divideByCamelHumps: Boolean = false,
wordSeparator: Char = ' ',
) { /*...*/ }
我们既可以直接传入一个 String
来调用这个参数:
reformat("This is a long String!")
也可以通过提供具名参数,传入几个可选参数值:
代码语言:javascript复制reformat("This is a short String!", upperCaseFirstLetter = false, wordSeparator = '_')
无论如何,他们都会正常工作。
自定义顺序?
但是,考虑如下情况:
代码语言:javascript复制fun main(args: Array<String>) {
var i0 = 0
myPrint(
a = i0,
b = i0,
c = i0,
)
i0 = 0
myPrint(
c = i0,
b = i0,
a = i0,
)
i0 = 0
myPrint( i0, i0, i0)
}
private fun myPrint(a:Int,b:Int,c:Int){
println("a=a, b=b, c=$c")
}
myPrint
函数是一个很简单的函数,它单纯向我们输出传入的 a,b,c
三个参数的值。在本例中,我们调用了三次 myPrint
函数,前两次通过提供具名参数的方式调用,但两次传入的具名参数顺序略有不同:一次是 a,b,c
,一次是 c,b,a
,第三个则很简单,直接按顺序传入了参数。
那么问题是:我们得到的输出结果,是会按照具名参数顺序执行,还是按照方法形参顺序执行呢?
经过测试,我们得到了这样的结果:
代码语言:javascript复制a=1, b=2, c=3
a=3, b=2, c=1
a=1, b=2, c=3
这也就意味着,Kotlin 会按照传入的具名参数顺序来传递实参,而不是按照形参顺序
原理揭秘
这其实很有意思,对于 Javaer 们来说,一定程度上也很反直觉,通过反编译 JVM 字节码,我们揭开了其中的秘密:
代码语言:javascript复制DEFINE PUBLIC STATIC FINAL main([Ljava/lang/String; args)V
A:
ALOAD args
LDC "args"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter(Ljava/lang/Object;Ljava/lang/String;)V
B:
LINE B 2
ICONST_0
ISTORE i0
C:
LINE C 4
IINC i0 1
ILOAD i0
D:
LINE D 5
IINC i0 1
ILOAD i0
E:
LINE E 6
IINC i0 1
ILOAD i0
F:
LINE F 3
INVOKESTATIC MainKt.myPrint(III)V
G:
LINE G 9
ICONST_0
ISTORE i0
H:
LINE H 12
IINC i0 1
ILOAD i0
ISTORE 2
I:
LINE I 13
IINC i0 1
ILOAD i0
ISTORE 3
J:
LINE J 14
IINC i0 1
ILOAD i0
K:
LINE K 13
ILOAD 3
L:
LINE L 12
ILOAD 2
M:
LINE M 11
INVOKESTATIC MainKt.myPrint(III)V
N:
LINE N 17
ICONST_0
ISTORE i0
O:
LINE O 19
IINC i0 1
ILOAD i0
IINC i0 1
ILOAD i0
IINC i0 1
ILOAD i0
INVOKESTATIC MainKt.myPrint(III)V
P:
LINE P 20
RETURN
Q:
代码语言:javascript复制// Decompiled with: FernFlower
// Class Version: 8
import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;
@Metadata(
mv = {1, 6, 0},
k = 2,
xi = 48,
d1 = {"u0000nu0000nnu0000nnnbnbnbu000002fb00¢ 020b2t0b2n0bH¨"},
d2 = {"main", "", "args", "", "", "([Ljava/lang/String;)V", "myPrint", "a", "", "b", "c", "TestKotlin"}
)
public final class MainKt {
public static final void main(@NotNull String[] args) {
Intrinsics.checkNotNullParameter(args, "args");
int i0 = 0;
i0;
myPrint(i0 , i0 , i0);
i0 = 0;
i0;
int var2 = i0 ;
int var3 = i0 ;
myPrint(i0, var3, var2);
i0 = 0;
i0;
myPrint(i0 , i0 , i0);
}
private static final void myPrint(int a, int b, int c) {
System.out.println("a=" a ", b=" b ", c=" c);
}
}
其实,Kotlin 在编译时,会帮我们创建几个中间变量,提前计算这些中间变量的值,然后再按照我们所要求的顺序传入实参。
后记
当我的 Recaf 使用默认的 Procyon 作为 Decompiler 的时候,得到了非常诡异的结果:
代码语言:javascript复制// Decompiled with: Procyon 0.6.0
// Class Version: 8
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;
import kotlin.Metadata;
@Metadata(mv = { 1, 6, 0 }, k = 2, xi = 48, d1 = { "u0000nu0000nnu0000nnnbnbnbu000002fb00¢ 020b2t0b2n0bH¨" }, d2 = { "main", "", "args", "", "", "([Ljava/lang/String;)V", "myPrint", "a", "", "b", "c", "TestKotlin" })
public final class MainKt
{
public static final void main(@NotNull final String[] args) {
Intrinsics.checkNotNullParameter(args, "args");
int i0 = 0;
myPrint( i0, i0, i0);
i0 = 0;
myPrint( i0, i0, i0);
i0 = 0;
myPrint( i0, i0, i0);
}
private static final void myPrint(final int a, final int b, final int c) {
System.out.println((Object)("a=" a ", b=" b ", c=" c));
}
}
而这个反编译结果运行下来,得到的结果是和 Kotlin 完全不同的:
代码语言:javascript复制a=1, b=2, c=3
a=1, b=2, c=3
a=1, b=2, c=3
吓得我以为 Kotlin 在解释环节干了什么奇怪的东西,使得相同的字节码在 Kotlin 和 Java 环境下产生了完全不同的结果