Swift协议可以通过向它们添加扩展来为函数和计算属性提供默认实现。我做了很多次。我的理解是,默认实现仅用作“后备”:当类型符合协议但不提供自己的实现时执行。
至少那是我如何阅读The Swift Programming Language指南:
如果符合类型提供其自己的必需方法或属性的实现,则将使用该实现而不是扩展提供的实现。
现在我遇到了这样一种情况:我实现特定协议的自定义类型确实为特定函数提供了一个实现,但它没有被执行 - 而是执行协议扩展中定义的实现。
举个例子,我定义了一个协议Movable
,它有一个函数move(to:)
和一个为这个函数提供默认实现的扩展:
protocol Movable {
func move(to point: CGPoint)
}
extension Movable {
func move(to point: CGPoint = CGPoint(x: 0, y: 0)) {
print("Moving to origin: \(point)")
}
}
接下来,我定义了一个符合Car
的Movable
类,但为move(to:)
函数提供了自己的实现:
class Car: Movable {
func move(to point: CGPoint = CGPoint(x: 0, y: 0)) {
print("Moving to point: \(point)")
}
}
现在我创建一个新的Car
并将其作为Movable
向下传播:
let castedCar = Car() as Movable
根据我是否传递可选参数point
的值,我观察到两种不同的行为:
Car
的实现被称为:
castedCar.move(to: CGPoint(x: 20, y: 10))
输出:
移至点:(20.0,10.0)move()
函数而不提供可选参数的值时,Car
的实现将被忽略,
→改为调用Movable
协议的默认实现:
castedCar.move()
输出:
移至原点:(0.0,0.0)这是因为这个电话
castedCar.move(to: CGPoint(x: 20, y: 10))
能够被解析为协议要求func move(to point: CGPoint)
- 因此调用将通过协议见证表(协议类型值实现多态的机制)动态调度,允许调用Car
的实现。
然而,电话
castedCar.move()
与协议要求func move(to point: CGPoint)
不匹配。因此,不会通过协议见证表(仅包含协议要求的方法条目)来调度它。相反,当castedCar
被输入为Movable
时,编译器将不得不依赖静态调度。因此,将调用协议扩展中的实现。
默认参数值仅仅是函数的静态特征 - 编译器实际上只会发出一个函数的重载(一个包含所有参数)。尝试通过排除其中一个具有默认值的参数来应用函数将触发编译器插入对该默认参数值的评估(因为它可能不是常量),然后在调用站点插入该值。
因此,具有默认参数值的函数不能很好地与动态调度一起使用。使用默认参数值替换方法的类也可以获得意外结果 - 请参阅this bug report。
获得默认参数值所需的动态调度的一种方法是在协议中定义static
属性要求,以及协议扩展中的move()
重载,该扩展仅使用move(to:)
。
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)
因为defaultMoveToPoint
现在是一个协议要求 - 它可以动态调度,从而为您提供所需的行为。
作为附录,请注意我们在defaultMoveToPoint
而不是type(of: self)
上调用Self
。这将为我们提供实例的动态元类型值,而不是调用方法的静态元类型值,从而确保正确调度defaultMoveToPoint
。但是,如果调用任何move()
的静态类型(Moveable
本身除外)就足够了,你可以使用Self
。
我将更详细地介绍in this Q&A中协议扩展中可用的动态和静态元类型值之间的差异。