覆盖 Swift 扩展中的方法

Posted

技术标签:

【中文标题】覆盖 Swift 扩展中的方法【英文标题】:Overriding methods in Swift extensions 【发布时间】:2016-11-07 20:56:06 【问题描述】:

我倾向于只将必需品(存储的属性、初始化程序)放入我的类定义中,并将其他所有内容移入它们自己的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 设为根类,编译器会抱怨说declarations in extensions cannot override yet,我已经读过。但是将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 再次解决了我的问题。

理论上我可以将dynamic 添加到我曾经做过的所有overrides 中,但我觉得我在这里做错了什么。

像我这样使用extensions 对代码进行分组真的错了吗?

【问题讨论】:

以这种方式使用扩展是 Swift 的惯例。甚至 Apple 在标准库中也这样做。 github.com/apple/swift/blob/master/docs/… @AMomchilov 你链接的文件谈到了协议,我错过了什么吗? 我怀疑这两种机制都适用 似乎重复了Swift dispatch to overridden methods in subclass extensions。马特的回答是这是一个错误(他引用了文档来支持这一点)。 【参考方案1】:

扩展不能/不应该覆盖。

如 Apple 的 Swift 指南中所述,无法覆盖扩展中的功能(如属性或方法)。

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

Swift Developer Guide

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

?这让我想起了艾萨克·阿西莫夫的“Three Laws of Robotics”?

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

所以分组没有什么问题,但你应该在类中而不是在扩展中覆盖。

指令说明

如果方法与 Objective-C 兼容,则您只能在子类的扩展中 override 一个超类方法,即 load() initialize()

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

所有 Swift 应用程序都在 Objective-C 运行时内执行,但使用纯 Swift-only 框架时除外,它允许纯 Swift 运行时。

我们发现,在应用进程中初始化类时,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 的原因。

【讨论】:

扩展不能只覆盖类中定义的方法。它可以覆盖父类中定义的方法。 -Swift3-嗯,这很奇怪,因为您还可以从您包含的框架中覆盖(这里的覆盖是指像 Swizzling 之类的东西)方法。即使这些框架是用纯 swift 编写的......也许框架也绑定到 objc,这就是为什么? @tymac 我不明白。如果 Objective-C 运行时 需要一些东西来兼容 Objective-C,为什么 Swift 编译器 仍然允许在扩展中覆盖?将 Swift 扩展中的覆盖标记为语法错误如何损害 Objective-C 运行时? 太令人沮丧了,所以基本上当你想用已经在项目中的代码创建一个框架时,你将不得不子类化并重命名所有内容...... @AuRis 你有什么参考吗?【参考方案2】:

Swift 的目标之一是静态调度,或者说减少动态调度。然而,Obj-C 是一种非常动态的语言。您所看到的情况源于两种语言之间的联系以及它们协同工作的方式。它不应该真正编译。

关于扩展的要点之一是它们用于扩展,而不是用于替换/覆盖。从名称和文档中可以清楚地看出这是意图。实际上,如果您从代码中取出指向 Obj-C 的链接(删除 NSObject 作为超类),它将无法编译。

因此,编译器正试图决定它可以静态分派什么以及它必须动态分派什么,并且由于代码中的 Obj-C 链接,它正在落入一个空白。 dynamic 'works' 的原因是因为它强制 Obj-C 链接所有东西,所以它总是动态的。

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

【讨论】:

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

有一种方法可以实现类签名和实现(在扩展中)的清晰分离,同时保持在子类中具有覆盖的能力。诀窍是使用变量代替函数

如果您确保在单独的 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】:

这个答案不是针对 OP,除了我受到他的陈述的启发而做出回应之外,“我倾向于只将必需品(存储的属性,初始化程序)放入我的类定义中,并将其他所有内容移到他们的自己的扩展...”。我主要是一名 C# 程序员,在 C# 中可以为此目的使用部分类。例如,Visual Studio 使用部分类将与 UI 相关的内容放在单独的源文件中,并让您的主源文件保持整洁,这样您就不会分心。

如果您搜索“swift partial class”,您会发现各种链接,其中 Swift 的拥护者说 Swift 不需要分部类,因为您可以使用扩展。有趣的是,如果您在 Google 搜索字段中输入“swift extension”,它的第一个搜索建议是“swift extension override”,而目前这个 Stack Overflow 问题是第一个命中。我认为这意味着(缺乏)覆盖功能的问题是与 Swift 扩展相关的最常搜索的主题,并强调 Swift 扩展不可能替换部分类的事实,至少如果你在你的编程。

无论如何,为了缩短冗长的介绍,我遇到了这个问题,我想将一些样板/行李方法从我的 C#-to-Swift 程序所在的 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 的问题,但我希望这种简单的解决方法可能对希望将方法从主要源文件移动到扩展文件并运行的其他人有所帮助进入无覆盖问题。

【讨论】:

【参考方案5】:

使用 POP(面向协议的编程)覆盖扩展中的函数。

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()

【讨论】:

这假定程序员可以控制 AClass 的原始定义,因此它可以依赖 AProtocol。在想要覆盖 AClass 中的功能的情况下,通常情况并非如此(即 AClass 可能是 Apple 提供的标准库类)。 注意,如果您不想或不能修改类的原始定义,您可以(在某些情况下)在扩展或子类中应用协议。【参考方案6】:

只是想补充一点,对于 Objective-C 类,两个不同的类别最终可能会覆盖同一个方法,而这种情况……嗯……可能会发生意想不到的事情。

Objective-C 运行时不保证将使用哪个扩展,如 Apple here 所述:

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

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

【讨论】:

以上是关于覆盖 Swift 扩展中的方法的主要内容,如果未能解决你的问题,请参考以下文章

使用 Swift 协议和扩展覆盖对象方法

swift 1.2 覆盖 NSManagedObject 扩展中的 prepareForDeletion

覆盖在swift中的扩展对象内不起作用

覆盖在swift中的扩展对象内不起作用

如何覆盖子类的 swift 协议函数(例如 UIView 中的 UILabel)

Swift协议扩展覆盖