在Swift扩展中重写方法

159

我倾向于在我的类定义中只放置必需品(存储属性、初始化器),并将其他所有内容移动到它们自己的extension中,就像每个逻辑块一个extension一样,我也会将它们分组与// MARK:一起。

例如,对于UIView子类,我最终会得到一个与布局相关的扩展,一个用于订阅和处理事件等等。 在这些扩展中,我不可避免地要覆盖一些UIKit方法,例如layoutSubviews。 我从未注意到这种方法存在任何问题-直到今天。

以这个类层次结构为例:

public class C: NSObject {
    public func method() { print("C") }
}

public class B: C {
}
extension B {
    override public func method() { print("B") }
}

public class A: B {
}
extension A {
    override public func method() { print("A") }
}

(A() as A).method()
(A() as B).method()
(A() as C).method()
输出结果为 A B C。这对我来说没什么意义。我了解协议扩展是静态分派的,但这不是一个协议,这是一个普通的类,我期望在运行时方法调用会被动态分派。显然对于 C 的调用至少应该是动态分派并产生 C
如果我从 NSObject 中移除继承并将 C 变为根类,编译器会抱怨说“extensions中的声明不能覆盖”,我已经了解过了。但是将 NSObject 作为根类如何改变事情呢?
将两个覆盖重写放入它们的类声明中会产生预期的输出 A A A,只移动 B 的会产生 A B B,只移动 A 的会产生 C B C,最后一个对我来说毫无意义:即使是静态类型为 A 的那个也不再产生 A 输出!
dynamic 关键字添加到定义或覆盖中似乎会给我所需的行为“从类层次结构的这一点开始往下”...
让我们将示例更改为一个稍微不那么构造的例子,这实际上促使我发布了这个问题:
public class B: UIView {
}
extension B {
    override public func layoutSubviews() { print("B") }
}

public class A: B {
}
extension A {
    override public func layoutSubviews() { print("A") }
}


(A() as A).layoutSubviews()
(A() as B).layoutSubviews()
(A() as UIView).layoutSubviews()

现在我们得到了A B A。但我无法通过任何方式使UIView的layoutSubviews动态化。

将这两个重写方法都放到它们所在的类声明中,我们又得到了A A A,只使用A或B仍然得到A B Adynamic再次解决了我的问题。

理论上,我可以将所有的override添加dynamic修饰,但我感觉我在做其他的错误操作。

像我这样使用extension分组代码真的是错误的吗?


使用这种方式的扩展是Swift的惯例。即使是Apple在标准库中也这样做。 - Alexander
https://github.com/apple/swift/blob/master/docs/GenericsManifesto.md#dynamic-dispatch-for-members-of-protocol-extensions - Alexander
1
@AMomchilov,您提供的文档讨论了协议,我有什么遗漏吗? - Christian Schnorr
我怀疑这是同样适用于两者的机制。 - Alexander
3
似乎是重复的问题,涉及到子类扩展中的Swift方法分派。那里的Matt回答说这是一个bug(并引用了文档来支持这一点)。 - jscs
显示剩余3条评论
6个回答

275

扩展不能/不应该覆盖。

在苹果的 Swift 指南中记录的一样,扩展无法覆盖功能(如属性或方法)。

扩展可以为类型添加新功能,但它们不能覆盖现有功能。

Swift 开发人员指南

编译器允许您在扩展中进行覆盖以与 Objective-C 兼容。但实际上,这违反了语言指令。

这让我想起了艾萨克·阿西莫夫的 "机器人三大定律"

扩展(语法糖)定义独立的方法,接收它们自己的参数。调用的函数,例如 layoutSubviews 取决于编译代码时编译器知道的上下文。UIView 继承自 UIResponder,后者继承自 NSObject,因此扩展中的覆盖是允许的,但不应该这样做

因此,分组没有问题,但应该在类中进行覆盖,而不是在扩展中进行。

指令注释

如果要在子类的扩展中覆盖超类方法,即load() initialize(),则该方法必须与Objective-C兼容。

因此,我们可以看看为什么允许您使用layoutSubviews进行编译。

所有Swift应用程序都在Objective-C运行时内执行,除非使用纯Swift-only框架,这使得只能使用Swift-only运行时。

正如我们所发现的那样,在初始化应用程序进程中的类时,Objective-C运行时通常会自动调用两个类主要方法load()initialize()

关于dynamic修饰符

来自Apple Developer Library (archive.org)

您可以使用dynamic修饰符,要求通过Objective-C运行时动态分派成员访问。

当Swift API被Objective-C运行时导入时,对于属性、方法、下标或初始化程序,没有动态分派的保证。 Swift编译器仍然可能取消虚拟化或内联成员访问以优化代码性能,绕过Objective-C运行时。

因此,dynamic可以应用于您的layoutSubviews -> UIView Class,因为它由Objective-C表示,并且始终使用Objective-C运行时访问该成员。

这就是为什么编译器允许您使用overridedynamic的原因。


9
扩展无法覆盖仅在类中定义的方法。它可以覆盖父类中定义的方法。 - RJE
-Swift3- 很奇怪,因为您可以覆盖(这里的覆盖是指Swizzling之类的操作)包含的框架中的方法,即使那些框架是用纯Swift编写的......也许框架也与objc绑定在一起,这就是为什么。 - farzadshbfn
@tymac 我不明白。如果 Objective-C runtime 为了 Objective-C 兼容性需要某些东西,那么为什么 Swift compiler 仍然允许在扩展中进行重写?将 Swift 扩展中的重写标记为语法错误如何损害 Objective-C runtime? - Alexander Vasenin
2
非常令人沮丧,基本上当您想要使用项目中已有的代码来创建一个框架时,您将不得不子类化并重命名所有内容... - thibaut noah
3
@AuRis,你有任何参考资料吗? - ricardopereira
显示剩余5条评论

20

Swift 的一个目标是静态分派,或者说减少动态分派。然而,Obj-C 是一种非常动态的语言。你看到的情况源于这两种语言之间的联系以及它们共同工作的方式。它实际上不应该编译通过。

扩展的主要点之一是用于扩展而不是替换/覆盖。从名称和文档中都可以看出这是意图。如果从代码中删除与 Obj-C 的链接(将NSObject作为超类),编译就无法通过。

因此,编译器正在尝试确定它可以静态分派什么,以及它必须动态分派什么,并且由于代码中的 Obj-C 链接的缘故,它落入了一个空隙。dynamic“起作用”的原因是因为它强制在所有内容上链接 Obj-C,因此始终是动态的。

因此,使用扩展进行分组并没有错,这很好,但在扩展中覆盖是错误的。任何覆盖应该在主类本身中,并调用扩展点。


1
这也适用于变量吗?例如,如果您要覆盖UINavigationController中的supportedInterfaceOrientations(以显示不同方向的不同视图),您应该使用自定义类而不是扩展吗?许多答案建议使用扩展来覆盖supportedInterfaceOrientations,但我很想得到澄清。谢谢! - Crashalot
@Crashalot 我想你的意思是“属性”,在 Objective-C 中,属性被实现为后台的 getter/setter 方法,因此相同的逻辑适用。 - Cristik

14

有一种方法可以在保持子类能够进行重写的能力的同时,实现类的签名和实现(在扩展中)的干净分离。这个技巧是使用变量代替函数。

如果你确保在单独的Swift源文件中定义每个子类,那么你可以使用计算变量来进行重写,同时通过扩展将相应的实现组织得更加清晰。这将绕过Swift的“规则”,使得类的API/签名能够整洁地组织在一个地方:

// ---------- BaseClass.swift -------------

public class BaseClass
{
    public var method1:(Int) -> String { return doMethod1 }

    public init() {}
}

// the extension could also be in a separate file  
extension BaseClass
{    
    private func doMethod1(param:Int) -> String { return "BaseClass \(param)" }
}

...

// ---------- ClassA.swift ----------

public class A:BaseClass
{
   override public var method1:(Int) -> String { return doMethod1 }
}

// this extension can be in a separate file but not in the same
// file as the BaseClass extension that defines its doMethod1 implementation
extension A
{
   private func doMethod1(param:Int) -> String 
   { 
      return "A \(param) added to \(super.method1(param))" 
   }
}

...

// ---------- ClassB.swift ----------
public class B:A
{
   override public var method1:(Int) -> String { return doMethod1 }
}

extension B
{
   private func doMethod1(param:Int) -> String 
   { 
      return "B \(param) added to \(super.method1(param))" 
   }
}

每个类的扩展都可以使用相同的方法名称进行实现,因为它们是私有的并且彼此不可见(只要它们在不同的文件中)。

正如你所看到的,继承(使用变量名)可以通过super.variablename正常工作。

BaseClass().method1(123)         --> "BaseClass 123"
A().method1(123)                 --> "A 123 added to BaseClass 123"
B().method1(123)                 --> "B 123 added to A 123 added to BaseClass 123"
(B() as A).method1(123)          --> "B 123 added to A 123 added to BaseClass 123"
(B() as BaseClass).method1(123)  --> "B 123 added to A 123 added to BaseClass 123"

4
我想这对我的自有方法适用,但不适用于在我的类中覆盖系统框架方法。 - Christian Schnorr
1
这让我走上了一个正确的道路,为属性包装器条件协议扩展。谢谢! - Chris Prince
你能详细说明一下吗?“这使我走上了正确的路径,为属性包装器条件协议扩展做铺垫。” - Peter Lapisu

3
使用POP(Protocol-Oriented Programming)在扩展中重写函数。
protocol AProtocol {
    func aFunction()
}

extension AProtocol {
    func aFunction() {
        print("empty")
    }
}

class AClass: AProtocol {

}

extension AClass {
    func aFunction() {
        print("not empty")
    }
}

let cls = AClass()
cls.aFunction()

1
这假设程序员控制 AClass 的原始定义,以便它可以依赖于 AProtocol。在想要覆盖 AClass 中的功能的情况下,通常不是这种情况(即,AClass 可能是由 Apple 提供的标准库类)。 - Jonathan Leonard
请注意,在某些情况下,如果您不想或无法修改类的原始定义,则可以在扩展或子类中应用协议。 - shim

3

这个回答并不是针对OP的,只是因为他说“我倾向于仅在我的类定义中放置必需品(存储属性、初始化程序),并将其他所有内容移动到它们自己的扩展中...”而感到受到启发而作出回应。我主要是一名C#程序员,在C#中可以使用部分类来实现这个目的。例如,Visual Studio使用部分类将与UI相关的内容放在单独的源文件中,并使您的主源文件保持整洁,以免分心。

如果您搜索“swift partial class”,您会找到各种链接,Swift支持者说Swift不需要部分类,因为您可以使用扩展。有趣的是,如果您在Google搜索字段中键入“swift extension”,它的第一个搜索建议是“swift extension override”,目前这个Stack Overflow问题是第一个命中的。我认为这意味着与Swift扩展相关的(缺乏)覆盖功能问题是最受关注的主题,并强调了Swift扩展无法取代部分类,至少如果您在编程中使用派生类。

总之,为了简化冗长的介绍,我在一个情况下遇到了这个问题,即我想将一些样板/包袱方法移出Swift类的主要源文件,这些类是我的C#-to-Swift程序生成的。在将它们移动到扩展后遇到了无法覆盖这些方法的问题之后,我最终实现了以下简单的解决方法。主要的Swift源文件仍然包含一些小的存根方法,调用扩展文件中的真实方法,并且这些扩展方法被赋予唯一的名称以避免覆盖问题。

public protocol PCopierSerializable {

   static func getFieldTable(mCopier : MCopier) -> FieldTable
   static func createObject(initTable : [Int : Any?]) -> Any
   func doSerialization(mCopier : MCopier)
}

.

public class SimpleClass : PCopierSerializable {

   public var aMember : Int32

   public init(
               aMember : Int32
              ) {
      self.aMember = aMember
   }

   public class func getFieldTable(mCopier : MCopier) -> FieldTable {
      return getFieldTable_SimpleClass(mCopier: mCopier)
   }

   public class func createObject(initTable : [Int : Any?]) -> Any {
      return createObject_SimpleClass(initTable: initTable)
   }

   public func doSerialization(mCopier : MCopier) {
      doSerialization_SimpleClass(mCopier: mCopier)
   }
}

.

extension SimpleClass {

   class func getFieldTable_SimpleClass(mCopier : MCopier) -> FieldTable {
      var fieldTable : FieldTable = [ : ]
      fieldTable[376442881] = { () in try mCopier.getInt32A() }  // aMember
      return fieldTable
   }

   class func createObject_SimpleClass(initTable : [Int : Any?]) -> Any {
      return SimpleClass(
                aMember: initTable[376442881] as! Int32
               )
   }

   func doSerialization_SimpleClass(mCopier : MCopier) {
      mCopier.writeBinaryObjectHeader(367620, 1)
      mCopier.serializeProperty(376442881, .eInt32, { () in mCopier.putInt32(aMember) } )
   }
}

.

public class DerivedClass : SimpleClass {

   public var aNewMember : Int32

   public init(
               aNewMember : Int32,
               aMember : Int32
              ) {
      self.aNewMember = aNewMember
      super.init(
                 aMember: aMember
                )
   }

   public class override func getFieldTable(mCopier : MCopier) -> FieldTable {
      return getFieldTable_DerivedClass(mCopier: mCopier)
   }

   public class override func createObject(initTable : [Int : Any?]) -> Any {
      return createObject_DerivedClass(initTable: initTable)
   }

   public override func doSerialization(mCopier : MCopier) {
      doSerialization_DerivedClass(mCopier: mCopier)
   }
}

.

extension DerivedClass {

   class func getFieldTable_DerivedClass(mCopier : MCopier) -> FieldTable {
      var fieldTable : FieldTable = [ : ]
      fieldTable[376443905] = { () in try mCopier.getInt32A() }  // aNewMember
      fieldTable[376442881] = { () in try mCopier.getInt32A() }  // aMember
      return fieldTable
   }

   class func createObject_DerivedClass(initTable : [Int : Any?]) -> Any {
      return DerivedClass(
                aNewMember: initTable[376443905] as! Int32,
                aMember: initTable[376442881] as! Int32
               )
   }

   func doSerialization_DerivedClass(mCopier : MCopier) {
      mCopier.writeBinaryObjectHeader(367621, 2)
      mCopier.serializeProperty(376443905, .eInt32, { () in mCopier.putInt32(aNewMember) } )
      mCopier.serializeProperty(376442881, .eInt32, { () in mCopier.putInt32(aMember) } )
   }
}

正如我在介绍中所说的,这并没有真正回答OP的问题,但我希望这个简单的解决方法对那些希望将方法从主源文件移动到扩展文件并遇到无法覆盖的问题的其他人有所帮助。


0

只是想补充一下,对于Objective-C类,两个不同的分类可能会覆盖相同的方法,在这种情况下...嗯...可能会发生意外的事情。

正如苹果在此处所述,Objective-C运行时不保证使用哪个扩展:

如果在类别中声明的方法名称与原始类中的方法名称相同,或者与同一类别中的另一个方法(甚至是超类)中的方法名称相同,则在运行时使用哪个方法实现的行为是未定义的。如果您正在使用自己的类别与类一起使用,则这种情况不太可能成为问题,但是在使用类别向标准Cocoa或Cocoa Touch类添加方法时可能会导致问题。

对于纯Swift类,禁止这种行为是一件好事,因为这种过度动态的行为是难以检测和调查错误的潜在来源。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接