6

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我观察到两种截然不同的行为:


  1. 当通过点为可选参数

    Car的实现被称为

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

    输出:

    移至点:(20.0,10。0)


  • 当我调用move()功能而无需为可选参数提供值Car的实现将被忽略并且

    Movable协议的默认实现被调用,而不是

    castedCar.move() 
    

    输出:

    移动到原点:(0.0,0.0)


  • 为什么?

    +3

    我相信编译时会添加默认值,因此必须使用该变量的静态类型。 – Sulthan

    +1

    您正在将Car转换为Movable,因此它将调用Movable方法。如果您没有将汽车投入移动,它会调用您的汽车的移动方法 –

    +0

    @LeoDabus:如果我没有将'汽车'移动到'移动',我不会遇到这个问题。然而,当我执行演员时出现问题的原因是我的问题的关键。在一个实际的面向协议的实现中,我不知道我正在处理的对象的实际类 - 我只知道它符合'Movable'协议。 'let castedCar = Car()as'Movable'这行就是模仿这种情况的一种手段,以保持示例代码的清洁。 – Mischa

    回答

    8

    这仅仅是由于这样的事实,呼叫

    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(to:)一个move()超载一起。

    protocol Moveable { 
        static var defaultMoveToPoint: CGPoint { get } 
        func move(to point: CGPoint) 
    } 
    
    extension Moveable { 
    
        static var defaultMoveToPoint: CGPoint { 
         return .zero 
        } 
    
        // simply apply move(to:) with our given defined default. 
        // as 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)上拨打defaultMoveToPoint而不是Self。这将为我们提供实例的动态元类型值,而不是调用方法的静态元类型值,从而确保defaultMoveToPoint被正确调度。但是,如果调用move()的静态类型(除了Moveable本身)就足够了,则可以使用Self

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

    +0

    谢谢你这个非常详尽的答案!我从中学到了很多东西。在Swift编程语言指南中应该有一个关于这种行为的说明,因为它不是非常简单的预期行为(除非你知道一些编译器细节,即静态和动态调度)。 – Mischa

    +0

    所以一般来说**的默认功能参数不适用于协议**。它们总是被静态地解析,因此它总是被调用的协议_extension_的实现,因为这是编译器在构建时“可见”的。 – Mischa

    +2

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

    相关问题