【问题标题】:Default arguments vs overloads, when to use which默认参数与重载,何时使用哪个
【发布时间】:2017-02-05 15:02:21
【问题描述】:

在 Kotlin 中,有两种方法可以表示可选参数,或者通过指定默认参数值:

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

或通过引入重载:

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

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

这种功能的消费者有什么不同?

【问题讨论】:

  • 注意:虽然有与其他语言相关的类似问题,例如 C#VB.NET,但此问题是 Kotlin 特有的。
  • 永远不要为此使用重载,就是这样。 @JVMOverloads 可能有助于从 Java 中使用它们

标签: overloading kotlin optional-parameters


【解决方案1】:

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

使用默认值的特殊情况:

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

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

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

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

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

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

    对于每个具有默认值的参数,这将生成一个额外的重载,该重载会删除该参数及其右侧的所有参数。

  • 您有以前版本的库,为了二进制 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# 中,它是在该语言的生命周期中较晚引入的,因此具有更多“可选采用”的感觉

【讨论】:

  • 我不知道 Kotlin,但是在调用 foo() 时是否可以传递非默认 Y 值而保留 X 默认值?如果不是,这似乎是个问题,因为您需要在代码中的多个位置复制 X 的默认值。
  • @BobBrinks 这在 Kotlin 文档中有明确的介绍,您可以传递一些值而不是其他值。您可以使用有序参数或命名参数来执行此操作,具体取决于参数列表的定义方式。
【解决方案2】:

让我们检查一下在 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

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

【讨论】:

    【解决方案3】:

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

    考虑以下示例:

    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)
    

    【讨论】:

      猜你喜欢
      • 2018-07-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-18
      • 1970-01-01
      相关资源
      最近更新 更多