Many thanks to Stefano De Carolis for the tip!

Swift allows function overloading, a.k.a. it lets us declare two or more functions with the same name but different arguments and/or return types:

func sameName() {
  ...
}

func sameName(parameter: Int) {
  ...
}

In the example above it’s always clear which function is called based on the parameter presence:

sameName() // no parameter => calls sameName()
sameName(parameter: 1) // parameter => calls sameName(parameter:)

This is pretty straight forward and expected behavior.

Swift however has many different ways to overload a function, which can bring to ambiguous situations where the behavior is not so easy to predict, for example:

Void vs default value

func sameName() {
  ...
}

func sameName(value: Int = 5) {
  ...
}

A function accepts no parameters and the other accepts one with a default value:

sameName(value: 1) // parameter passed => calls sameName(value:)
sameName() // no parameter passed, both declarations match => ??

Protocol vs conforming type of that protocol

func sameName(_ number: Int) {
  ...
}

func sameName<N: Numeric>(_ number: N) {
  ...
}

Both functions accept a parameter, one requires an Int instance while the other requires an instance conforming to Numeric (which Int conforms to):

sameName(5.0) // not an Int => calls Numeric function
sameName(5) // Int, both declarations match => ??

Variadic vs non

func sameName(_ number: Int) {
  ...
}

func sameName(_ numbers: Int...) {
  ...
}

Here we accept an Int in one function, and zero or more Ints in the other (a.k.a, we have a variadic parameter):

sameName(5, 5, 5) // Multiple values are passed => variadic function
sameName(5) // One value is passed, both declarations match => ??

The hidden principle

It’s quite simple to find ourselves in an ambiguous situation, while not officially documented, former Swift team member Jordan Rose has shed some light on the secret formula:

“The general principle is that the most specific overload should win”.

Going back to our examples:

Void vs default value

func sameName()
func sameName(value: Int = 5)

sameName() triggers the first function:
a function that always takes 0 parameters is more specific than a function that sometimes takes 0 parameters.

Protocol vs conforming type of that protocol

func sameName(_ number: Int)
func sameName<N: Numeric>(_ number: N)

sameName(5) triggers the first function:
a function accepting Int is more specific than a function accepting any type conforming to Numeric.

Variadic vs non

func sameName(_ number: Int)
func sameName(_ numbers: Int...)

sameName(5) triggers the first function:
a function that always takes 1 parameter is more specific than a function that takes 0+ parameters.

@_disfavoredOverload

The underscore in front of the attribute means that this Swift feature hasn’t gone through Swift Evolution just yet, proceed at your own discretion.

Thanks to this principle we now have a nice way to predict Swift’s behavior in case of ambiguity.

While this is great, sometimes as API/library designers the default behavior is not what we would like to have:
in such situations we can use the @_disfavoredOverload attribute, which will lower the rank of the associated function, making less specific functions win over more specific ones.

Going back to an earlier example:

func sameName() { ... }
func sameName(value: Int = 5) { ... }

we know that calling sameName() matches the first function, however, if we introduce @_disfavoredOverload, this changes:

@_disfavoredOverload 
func sameName() { ... }

func sameName(value: Int = 5) { ... }

With this updated declaration calling sameName() will match the second function instead.

It’s very important to note that this attribute won’t show up in our library headers:
if we use this attribute on our public declarations, it’s recommended to document the behavior, which otherwise may cause surprises to adopters of our API.

SwiftUI

SwiftUI relies on @_disfavoredOverload in many of its declarations.

An example is Text, which we covered in Displaying text in SwiftUI. Text comes with multiple initializers, let’s focus on two in particular:

public init<S: StringProtocol>(_ content: S)
public init(_ key: LocalizedStringKey, tableName: String? = nil, bundle: Bundle? = nil, comment: StaticString? = nil)

From what we’ve seen so far we know that calling Text("a string") would match the first declaration, however, if we read init<S: StringProtocol>(_ content: S)’s documentation, we will see the following message:

/// SwiftUI doesn't call the `init(_:)` method when you initialize a text
/// view with a string literal as the input. Instead, a string literal
/// triggers the ``Text/init(_:tableName:bundle:comment:)`` method — which
/// treats the input as a ``LocalizedStringKey`` instance — and attempts to
/// perform localization.

This works because LocalizedStringKey conforms to ExpressibleByStringLiteral:
as this key is the only required parameter in init(_:tableName:bundle:comment:), a definition such as Text("a string") matches both initializers, and associating @_disfavoredOverload to the StringProtocol’s init makes the other initializer the preferred one.

Again, like in Text’s case, it’s very important to document such behavior as otherwise adopters of our API would be met with unexpected results.

The reason behind this Text behavior is to make SwiftUI’s API easy to use:

  • if we pass a string literal (e.g. Text("my_title")), the framework will turn the string into a LocalizedStringKey and attempt to localize it
  • if we don’t pass a string literal (e.g. Text(stringVariable)), the framework will display the value as is, without localization

Conclusions

Function overloading is a powerful tool that enables us to define multiple functions of the same name with different implementations:
in this article we’ve covered how Swift addresses ambiguity and how we can (cautiously) use the @_disfavoredOverload attribute in case of multiple matches.

Do you use this approach yourself? Have you seen any other cool use of @_disfavoredOverload? Please let me know!

Thank you for reading and stay tuned for more articles!

⭑⭑⭑⭑⭑