默认参数与重载,何时使用哪个

Default arguments vs overloads, when to use which

在 Kotlin 中有两种方式来表达可选参数,要么通过指定默认参数值:

fun foo(parameter: Any, option: Boolean = false) { ... }

或引入重载:

fun foo(parameter: Any) = foo(parameter, false)
fun foo(parameter: Any, option: Boolean) { ... }

在哪些情况下首选哪种方式?

这样的功能对于消费者来说有什么区别?

在 Kotlin 代码中调用其他 Kotlin 代码可选参数往往是使用重载的规范。使用可选参数应该是您的默认行为。

使用默认值的特殊情况:

  • 作为一般做法或如果不确定 - 使用默认参数覆盖。

  • 如果您希望调用者看到默认值,请使用默认值。它们将出现在 IDE 工具提示中( 即 Intellij IDEA)并让调用者知道它们正在作为合同的一部分应用。您可以在以下屏幕截图中看到,如果 xy 的值被省略,则调用 foo() 将默认一些值:

    而对函数重载做同样的事情会隐藏这些有用的信息,只会呈现出更加混乱的情况:

  • 使用默认值会导致生成两个函数的字节码,一个具有指定的所有参数,另一个是桥接函数,可以使用默认值检查和应用缺失的参数。无论你有多少默认参数,它总是只有两个函数。所以在总函数数受限的环境中(即Android),最好只拥有这两个函数而不是大量的重载完成同样的工作。

您可能不想使用默认参数值的情况:

  • 当您希望另一种 JVM 语言能够使用默认值时,您需要使用显式重载或使用 @JvmOverloads annotation which:

    For every parameter with a default value, this will generate one additional overload, which has this parameter and all parameters to the right of it in the parameter list removed.

  • 您有以前版本的库,为了二进制 API 兼容性,添加默认参数可能会破坏现有编译代码的兼容性,而添加重载则不会。

  • 您有一个以前存在的函数:

    fun foo() = ...
    

    并且您需要保留该函数签名,但您还想添加另一个具有相同签名但附加可选参数的函数:

    fun foo() = ...
    fun foo(x: Int = 5) = ...   // never can be called using default value
    

    您将无法在第二个版本中使用默认值(除了通过反射 callBy)。相反,所有 foo() 不带参数的调用仍然调用函数的第一个版本。所以你需要使用没有默认值的不同重载,否则你会混淆函数的用户:

    fun foo() = ...  
    fun foo(x: Int) = ...
    
  • 您的参数放在一起可能没有意义,因此重载允许您将参数分组到有意义的协调集中。

  • 调用具有默认值的方法必须执行另一个步骤来检查缺少哪些值并应用默认值,然后将调用转发给真正的方法。因此,在性能受限的环境中(即Android,嵌入式、实时、方法调用上的十亿次循环迭代),可能不需要这种额外的检查。尽管如果您在分析中没有看到问题,这可能是一个想象中的问题,可能由 JVM 内联,并且可能根本没有任何影响。先测量再担心。

不支持任何一种情况的情况:

如果您正在阅读其他语言关于此的一般性论点...

  • C# answer for this similar question 中,受人尊敬的 Jon Skeet 提到您应该小心使用默认值,如果它们可以在构建之间更改,那将是一个问题。在 C# 中,默认设置在调用站点,而在 Kotlin 中,对于非内联函数,默认设置在被调用的(桥接)函数内部。因此,对于 Kotlin 而言,更改隐藏和显式默认值的影响相同,并且此论点不应影响决策。

  • 也在 C# 回答中说,如果团队成员对使用默认参数有反对意见,那么也许不要使用它们。这不应该应用于 Kotlin,因为它们是核心语言功能,并且自 1.0 之前就在标准库中使用,并且不支持限制它们的使用。对方团队成员应该默认使用默认参数,除非他们有明确的案例使他们无法使用。而在 C# 中,它在该语言的生命周期中引入的时间要晚得多,因此具有更多 "optional adoption"

  • 的意义

让我们检查一下在 Kotlin 中如何编译具有默认参数值的函数,看看方法计数是否存在差异。它可能因目标平台而异,因此我们将首先研究用于 JVM 的 Kotlin。

对于函数fun foo(parameter: Any, option: Boolean = false),生成以下两个方法:

  • 首先是 foo(Ljava/lang/Object;Z)V,当在调用站点指定所有参数时调用它。
  • 其次是synthetic bridge foo$default(Ljava/lang/Object;ZILjava/lang/Object;)V。它有 2 个附加参数:Int 掩码,指定实际传递了哪些参数和一个 Object 当前未使用但保留以允许将来使用默认参数进行超级调用的参数。

当在调用站点省略某些参数时,将调用该桥。桥分析掩码,为省略的参数提供默认值,然后调用现在指定所有参数的第一个方法。

当您在函数上放置 @JvmOverloads 注释时,会生成额外的重载,每个参数一个,具有默认值。所有这些重载委托给 foo$default 桥。对于 foo 函数,将生成以下附加重载:foo(Ljava/lang/Object;)V.

因此,从方法数的角度来看,在一个函数只有一个默认值参数的情况下,无论你使用重载还是默认值,你都会得到两个方法。但是如果有多个可选参数,使用默认值而不是重载会导致生成的方法更少。

当函数的实现在省略参数时变得更简单时,重载可能是首选。

考虑以下示例:

fun compare(v1: T, v2: T, ignoreCase: Boolean = false) =
    if (ignoreCase) 
        internalCompareWithIgnoreCase(v1, v2) 
    else
        internalCompare(v1, v2)

当它像 compare(a, b) 一样被调用并且 ignoreCase 被省略时,您实际上为不使用 ignoreCase 支付了两次费用:首先是检查参数并替换默认值而不是省略的第二个是当您检查 compare 正文中的 ignoreCase 并根据其值分支到 internalCompare 时。

添加重载将摆脱这两项检查。此外,具有如此简单主体的方法更有可能被 JIT 编译器内联。

fun compare(v1: T, v2: T) = internalCompare(v1, v2)