本文最后更新于 961 天前,其中的信息可能已经有所发展或是发生改变。
有关 Kotlin 具名参数形参传参顺序导致输出结果发生改变问题的一些探索
具名参数
众所周知,Kotlin 拥有一种叫做具名参数(Named arguments)的特性,它在需要跳过可选参数,或是调整参数顺序的地方十分有效。
例如如下拥有五个参数,且后四个参数为可选参数的函数:
fun reformat(
str: String,
normalizeCase: Boolean = true,
upperCaseFirstLetter: Boolean = true,
divideByCamelHumps: Boolean = false,
wordSeparator: Char = ' ',
) { /*...*/ }
我们既可以直接传入一个 String
来调用这个参数:
reformat("This is a long String!")
也可以通过提供具名参数,传入几个可选参数值:
reformat("This is a short String!", upperCaseFirstLetter = false, wordSeparator = '_')
无论如何,他们都会正常工作。
自定义顺序?
但是,考虑如下情况:
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
,第三个则很简单,直接按顺序传入了参数。
那么问题是:我们得到的输出结果,是会按照具名参数顺序执行,还是按照方法形参顺序执行呢?
经过测试,我们得到了这样的结果:
a=1, b=2, c=3
a=3, b=2, c=1
a=1, b=2, c=3
这也就意味着,Kotlin 会按照传入的具名参数顺序来传递实参,而不是按照形参顺序
原理揭秘
这其实很有意思,对于 Javaer 们来说,一定程度上也很反直觉,通过反编译 JVM 字节码,我们揭开了其中的秘密:
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:
// 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 = {"\u0000\n\u0000\n\n\u0000\n\n\n\b\n\b\n\b\u000002\f\b00¢ 020\b2\t0\b2\n0\bH¨"},
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 的时候,得到了非常诡异的结果:
// 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 = { "\u0000\n\u0000\n\n\u0000\n\n\n\b\n\b\n\b\u000002\f\b00¢ 020\b2\t0\b2\n0\bH¨" }, 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 完全不同的:
a=1, b=2, c=3
a=1, b=2, c=3
a=1, b=2, c=3
吓得我以为 Kotlin 在解释环节干了什么奇怪的东西,使得相同的字节码在 Kotlin 和 Java 环境下产生了完全不同的结果