有关 Kotlin 具名参数形参传参顺序导致输出结果发生改变问题的一些探索
本文最后更新于 715 天前,其中的信息可能已经有所发展或是发生改变。

有关 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 环境下产生了完全不同的结果

扫码关注 HikariLan's Blog 微信公众号,及时获取最新博文!


微信公众号图片
暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇