I recently encountered this unexpected behaviour when working with protocols and optionals.
For some unknown reason to me, the compiler decides to map a property to its default protocol implementation when the object that contains the property is optional
Minimum reproducible example
protocol UIComponent {
var onTap: (() -> Void)? { get }
}
extension UIComponent {
var onTap: (() -> Void)? { nil }
}
struct Component: UIComponent {
let action: () -> Void = { }
}
extension Component {
var onTap: () -> Void { action }
}
let component: Component = .init()
let optionalComponent: Component? = .init()
dump(component.onTap) // - (Function)
dump(optionalComponent?.onTap) // - nil -- uses the default implementation of UIComponent
As you can see from the example, the type of Component.onTap is not an Optional compared to the requirement of the protocol.
If I change the type of Component.onTap to an optional closure, I won't get a nil value anymore.
The code I wrote above only serves as a way to reproduce this unexpected behaviour.
The actual scenario is a little bit more complex, and involved not breaking around a hundred unit tests in our massive codebase
No, it is not an implementation bug. In Swift, implementations of protocol requirements cannot differ in optionality. That is, if the requirement is a property of type T?, then it must be fulfilled by an implementation with a property of type T?, not T:
protocol P {
var v: Int? { get }
}
extension P {
var v: Int? { 42 }
}
struct S: P {
var v: Int { 21 } // this does not fulfill the requirement
}
func f<T: P> (_ t: T) {
print(t.v as Any)
}
f(S()) // prints "Optional(42)"
In far future language versions, we may consider changing this—see the discussion on a draft protocol witness matching mini-manifesto. This would be a source-breaking change, as users rely on the current behavior.
It is very difficult to reason about, and that is why it's not recommended to overloaded two properties of the same name but different types. Consider the following—also very confusing!
protocol P {
var v: Int? { get }
}
extension P {
var v: Int? { 42 }
}
protocol Q {
var v: String { get }
}
extension Q {
var v: String { "Hello, world!" }
}
struct S: P, Q { }
let s = S()
let optionalS = Optional(S())
print(s.v as Any)
print(optionalS?.v as Any)
dump(optionalComponent!.onTap) // Component.onTap called 🛑
dump(optionalComponent?.onTap) // UIComponent.onTap called
optionalComponent!.onTap() // Component.onTap is called 🛑
optionalComponent?.onTap() // Component.onTap is called 🛑
I haven’t looked too closely at this behavior but it doesn’t seem obviously buggy to me. It’s not that surprising that the same name ends up resolving to a different overload when used in a position that expects a name as opposed to a position that doesn’t.
protocol P {
var value: Int? { get }
}
extension P {
var value: Int? { 42 }
}
struct S: P {
var value: Int { 21 }
}
let optionalComponent: S? = .init()
print(optionalComponent!.value) // 21
print(optionalComponent?.value) // Optional(42)
I suspect what’s happening here is that when you use optional chaining instead of force unwrapping, the result of the overall expression is always going to be optional, and so the overload that produces an optional value is going to be considered a better match than the one that doesn’t.
Perhaps a bit unintuitive but I think it’s ‘correct’ given how overload resolution/ranking works. And as Xiaodi notes, it’s a good reason to not rely on return-type overloading.
Indeed we can't remove it. Can we soft deprecate overloading by return type and have a warning when it is used? That would discourage new uses of it (and suggest user refactor the old uses).