This is due to the fact that the call
castedCar.move(to: CGPoint(x: 20, y: 10))
is able to be resolved to the protocol requirement func move(to point: CGPoint)
– therefore the call will be dynamically dispatched to via the protocol witness table (the mechanism by which protocol-typed values achieve polymorphism), allowing Car
's implementation to be called.
However, the call
castedCar.move()
does not match the protocol requirement func move(to point: CGPoint)
. It therefore won't be dispatched to via the protocol witness table (which only contains method entries for protocol requirements). Instead, as castedCar
is typed as Movable
, the compiler will have to rely on static dispatch. Therefore the implementation in the protocol extension will be called.
Default parameter values are merely a static feature of functions – only a single overload of the function will actually be emitted by the compiler (one with all the parameters). Attempting to apply a function by excluding one of its parameters which has a default value will trigger the compiler to insert an evaluation of that default parameter value (as it may not be constant), and then insert that value at the call site.
For that reason, functions with default parameter values simply do not play well with dynamic dispatch. You can also get unexpected results with classes overriding methods with default parameter values – see for example this bug report.
One way to get the dynamic dispatch you want for the default parameter value would be to define a static
property requirement in your protocol, along with a move()
overload in a protocol extension which simply applies move(to:)
with it.
protocol Moveable {
static var defaultMoveToPoint: CGPoint { get }
func move(to point: CGPoint)
}
extension Moveable {
static var defaultMoveToPoint: CGPoint {
return .zero
}
// Apply move(to:) with our given defined default. Because defaultMoveToPoint is a
// protocol requirement, it can be dynamically dispatched to.
func move() {
move(to: type(of: self).defaultMoveToPoint)
}
func move(to point: CGPoint) {
print("Moving to origin: (point)")
}
}
class Car: Moveable {
static let defaultMoveToPoint = CGPoint(x: 1, y: 2)
func move(to point: CGPoint) {
print("Moving to point: (point)")
}
}
let castedCar: Moveable = Car()
castedCar.move(to: CGPoint(x: 20, y: 10)) // Moving to point: (20.0, 10.0)
castedCar.move() // Moving to point: (1.0, 2.0)
Because defaultMoveToPoint
is now a protocol requirement – it can be dynamically dispatched to, thus giving you your desired behaviour.
As an addendum, note that we're calling defaultMoveToPoint
on type(of: self)
rather than Self
. This will give us the dynamic metatype value for the instance, rather than the static metatype value of what the method is called on, ensuring defaultMoveToPoint
is dispatched correctly. If however, the static type of whatever move()
is called on (with the exception of Moveable
itself) is sufficient, you can use Self
.
I go into the differences between the dynamic and static metatype values available in protocol extensions in more detail in this Q&A.