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

Mis*_*cha 8 extension-methods protocols swift swift-extensions swift-protocols

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

至少我是如何阅读Swift编程语言指南的:

如果符合类型提供其自己的必需方法或属性的实现,则将使用该实现而不是扩展提供的实现.

现在我遇到了这样一种情况:我实现特定协议的自定义类型确实为特定函数提供了一个实现,但它没有被执行 - 而是执行协议扩展中定义的实现.


作为示例,我定义了一个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)")
    }

}
Run Code Online (Sandbox Code Playgroud)

接下来,我定义一个Car符合的类,Movable但为move(to:)函数提供了自己的实现:

class Car: Movable {

    func move(to point: CGPoint = CGPoint(x: 0, y: 0)) {
        print("Moving to point: \(point)")
    }

}
Run Code Online (Sandbox Code Playgroud)

现在我创建一个新的Car并将其向下转换为Movable:

let castedCar = Car() as Movable
Run Code Online (Sandbox Code Playgroud)

根据我是否传递可选参数的值,point我会观察到两种不同的行为:


  1. 传递可选参数的点时

    Car实施被称为:

    castedCar.move(to: CGPoint(x: 20, y: 10)) 
    
    Run Code Online (Sandbox Code Playgroud)

    输出:

    移至点:(20.0,10.0)


  1. 当我调用move()功能,而无需为可选参数提供一个值Car的执行将被忽略,

    Movable协议的默认实现是不是:

    castedCar.move()
    
    Run Code Online (Sandbox Code Playgroud)

    输出:

    移至原点:(0.0,0.0)


为什么?

Ham*_*ish 11

这是因为这个电话

castedCar.move(to: CGPoint(x: 20, y: 10))
Run Code Online (Sandbox Code Playgroud)

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

然而,电话

castedCar.move()
Run Code Online (Sandbox Code Playgroud)

不会匹配协议的要求func move(to point: CGPoint).因此,不会通过协议见证表(仅包含协议要求的方法条目)来调度它.相反,如同castedCar输入Movable,编译器将不得不依赖静态分派.因此,将调用协议扩展中的实现.

默认参数值仅仅是函数的静态特性 - 编译器实际上只会发出一个函数的重载(一个包含所有参数).尝试通过排除其中一个具有默认值的参数来应用函数将触发编译器插入对该默认参数值的评估(因为它可能不是常量),然后在调用站点插入该值.

因此,具有默认参数值的函数不能很好地与动态调度一起使用.类使用默认参数值覆盖方法时,您也可以获得意外结果 - 请参阅此错误报告.


获得默认参数值所需的动态调度的一种方法是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)
Run Code Online (Sandbox Code Playgroud)

因为defaultMoveToPoint现在是一个协议要求 - 它可以动态调度,从而为您提供所需的行为.

作为附录,请注意,我们调用defaultMoveToPointtype(of: self),而不是Self.这将为我们提供实例的动态元类型值,而不是调用方法的静态元类型值,确保defaultMoveToPoint正确分派.但是,如果move()调用的静态类型(除了Moveable它自身)就足够了,你可以使用Self.

我将在本问答中更详细地介绍协议扩展中可用的动态和静态元类型值之间的差异.

  • @Mischa乐于助人:)是的,一般来说,默认的函数参数值不适用于协议.如果您尝试通过排除其中一个参数来应用它们,它将不再符合协议要求,因此将失去动态分派.即使有*方法要求协议要求表达必须由具有给定默认参数值的函数来满足,该值的*评估*的实现仍将由编译器静态决定. (2认同)