有关 Kotlin 具名参数形参传参顺序导致输出结果发生改变问题的一些探索

2022-10-27 10:01:31 浏览数 (1)

本文最后更新于 172 天前,其中的信息可能已经有所发展或是发生改变。

有关 Kotlin 具名参数形参传参顺序导致输出结果发生改变问题的一些探索

具名参数

众所周知,Kotlin 拥有一种叫做具名参数(Named arguments)的特性,它在需要跳过可选参数,或是调整参数顺序的地方十分有效。

例如如下拥有五个参数,且后四个参数为可选参数的函数:

代码语言:javascript复制
fun reformat(
    str: String,
    normalizeCase: Boolean = true,
    upperCaseFirstLetter: Boolean = true,
    divideByCamelHumps: Boolean = false,
    wordSeparator: Char = ' ',
) { /*...*/ }

我们既可以直接传入一个 String 来调用这个参数:

代码语言:javascript复制
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 环境下产生了完全不同的结果

0 人点赞