Default arguments vs overloads, when to use which

2019-02-21 22:12发布

问题:

In Kotlin there are two ways to express an optional parameter, either by specifying default argument value:

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

or by introducing an overload:

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

Which way is preferred in which situations?

What is the difference for consumers of such function?

回答1:

In Kotlin code calling other Kotlin code optional parameters tend to be the norm over using overloads. Using optional parameters should be you default behavior.

Special cases FOR using defaulted values:

  • As a general practice or if unsure -- use default arguments over overrides.

  • if you want the default value to be seen by the caller, use default values. They will show up in IDE tooltips (i.e. Intellij IDEA) and let the caller know they are being applied as part of the contract. You can see in the following screenshot that calling foo() will default some values if values are omitted for x and y:

    Whereas doing the same thing with function overloads hides this useful information and just presents a much more messy:

  • using default values causes bytecode generation of two functions, one with all parameters specified and another that is a bridge function that can check and apply missing parameters with their defaulted values. No matter how many defaulted parameters you have, it is always only two functions. So in a total-function-count constrained environment (i.e. Android), it can be better to have just these two functions instead of a larger number of overloads that it would take to accomplish the same job.

Cases where you might not want to use default argument values:

  • When you want another JVM language to be able to use the defaulted values you either need to use explicit overloads or use the @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.

  • You have a previous version of your library and for binary API compatibility adding a default parameter might break compatibility for existing compiled code whereas adding an overload would not.

  • You have a previous existing function:

    fun foo() = ...
    

    and you need to retain that function signature, but you also want to add another with the same signature but additional optional parameter:

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

    You will not be able to use the default value in the 2nd version (other than via reflection callBy). Instead all foo() calls without parameters still call the first version of the function. So you need to instead use distinct overloads without the default or you will confuse users of the function:

    fun foo() = ...  
    fun foo(x: Int) = ...
    
  • You have arguments that may not make sense together, and therefore overloads allow you to group parameters into meaningful coordinated sets.

  • Calling methods with default values has to do another step to check which values are missing and apply the defaults and then forward the call to the real method. So in a performance constrained environment (i.e. Android, embedded, real-time, billion loop iterations on a method call) this extra check may not be desired. Although if you do not see an issue in profiling, this might be an imaginary issue, might be inlined by the JVM, and may not have any impact at all. Measure first before worrying.

Cases that don't really support either case:

In case you are reading general arguments about this from other languages...

  • in a C# answer for this similar question the esteemed Jon Skeet mentions that you should be careful using defaults if they could change between builds and that would be a problem. In C# the defaulting is at the call site, whereas in Kotlin for non-inlined functions it is inside of the (bridge) function being called. Therefore for Kotlin it is the same impact for changing hidden and explicit defaulting of values and this argument should not impact the decision.

  • also in the C# answer saying that if team members have opposing views about use of defaulted arguments then maybe don't use them. This should not be applied to Kotlin as they are a core language feature and used in the standard library since before 1.0 and there is no support for restricting their use. The opposing team members should default to using defaulted arguments unless they have a definitive case that makes them unusable. Whereas in C# it was introduced much later in the life cycle of that language and therefore had a sense of more "optional adoption"



回答2:

Let's examine how functions with default argument values are compiled in Kotlin to see if there's a difference in method count. It may differ depending on the target platform, so we'll look into Kotlin for JVM first.

For the function fun foo(parameter: Any, option: Boolean = false) the following two methods are generated:

  • First is foo(Ljava/lang/Object;Z)V which is being called when all arguments are specified at a call site.
  • Second is synthetic bridge foo$default(Ljava/lang/Object;ZILjava/lang/Object;)V. It has 2 additional parameters: Int mask that specifies which parameters were actually passed and an Object parameter which currently is not used, but reserved for allowing super-calls with default arguments in the future.

That bridge is called when some arguments are omitted at a call-site. The bridge analyzes the mask, provides default values for omitted arguments and then calls the first method now specifying all arguments.

When you place @JvmOverloads annotation on a function, additional overloads are generated, one per each argument with default value. All these overloads delegate to foo$default bridge. For the foo function the following additional overload will be generated: foo(Ljava/lang/Object;)V.

Thus, from the method count point of view, in a situation when a function has only one parameter with default value, it's no matter whether you use overloads or default values, you'll get two methods. But if there's more than one optional parameter, using default values instead of overloads will result in less methods generated.



回答3:

Overloads could be preferred when the implementation of a function gets simpler when parameter is omitted.

Consider the following example:

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

When it is called like compare(a, b) and ignoreCase is omitted, you actually pay twice for not using ignoreCase: first is when arguments are checked and default values are substituted instead of omitted ones and second is when you check the ignoreCase in the body of compare and branch to internalCompare based on its value.

Adding an overload will get rid of these two checks. Also a method with such simple body is more likely to be inlined by JIT compiler.

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