使用协议中定义的默认参数实现函数

Implementing a function with a default parameter defined in a protocol

Swift 协议可以通过向它们添加扩展来为函数和计算属性提供默认实现。我已经做过很多次了。据我了解,默认实现仅用作"fallback":当类型符合协议但不提供自己的实现时执行。

至少我是这样读的 The Swift Programming Language 指南:

If a conforming type provides its own implementation of a required method or property, that implementation will be used instead of the one provided by the extension.

现在我 运行 遇到这样一种情况,我的自定义类型实现了某个协议 确实 提供了特定功能的实现,但它没有被执行 — 定义的实现改为执行协议扩展中的内容。


举个例子,我定义了一个协议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 class 符合 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,我观察到两种截然不同的行为:


  1. 为可选参数

    传递点

    Car 的实现被称为:

    castedCar.move(to: CGPoint(x: 20, y: 10)) 
    

    输出:

    Moving to point: (20.0, 10.0)


  1. 当我调用 move() 函数 而没有为可选参数 提供值时, Car 的实现被忽略并且

    改为调用 Movable 协议的默认实现:

    castedCar.move()
    

    输出:

    Moving to origin: (0.0, 0.0)


为什么?

这是因为调用

castedCar.move(to: CGPoint(x: 20, y: 10))

能够解析为协议要求 func move(to point: CGPoint) – 因此调用将通过协议见证动态调度 table (协议类型值实现多态性的机制),允许调用 Car 的实现。

然而,调用

castedCar.move()

符合协议要求func move(to point: CGPoint)。因此,它不会通过协议见证 table(仅包含协议 requirements 的方法条目)发送给它。相反,由于 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 现在是一项协议要求 – 它可以动态调度,从而为您提供所需的行为。

作为附录,请注意我们在 type(of: self) 而不是 Self 上调用 defaultMoveToPoint。这将为我们提供实例的 dynamic 元类型值,而不是调用方法的静态元类型值,确保 defaultMoveToPoint 被正确调度。但是,如果调用 move() 的静态类型(Moveable 本身除外)就足够了,则可以使用 Self.

我更详细地探讨了协议扩展中可用的动态和静态元类型值之间的差异 in this Q&A