Swift 扩展中的重写方法

我倾向于只将必需品(存储属性、初始化器)放入我的类定义中,并将其他所有内容移动到它们自己的 extension中,有点像每个逻辑块使用 // MARK:进行分组的 extension

例如,对于 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 的布局 Subviews 动态的任何手段。

移动两个重写到他们的类声明得到我们再次 A A A,只有 A 的或只有 B 的仍然得到我们 A B Adynamic再次解决了我的问题。

理论上我可以把 dynamic加到我所做的所有 override中,但是我觉得我在这里做错了一些事情。

像我一样使用 extension对代码进行分组真的是错误的吗?

99670 次浏览

Swift 的目标之一是静态调度,或者更确切地说是减少动态调度。然而,Obj-C 是一种非常动态的语言。你所看到的情况是由这两种语言之间的联系和它们一起工作的方式造成的。它不应该真正编译。

关于扩展的一个要点是,它们是用于扩展的,而不是用于替换/重写。从名称和文档可以清楚地看出,这就是目的所在。实际上,如果从代码中删除到 Obj-C 的链接(删除 NSObject作为超类) ,它就不会编译。

因此,编译器正在尝试决定它可以静态分派哪些内容以及它必须动态分派哪些内容,由于代码中的 Obj-C 链接,编译器正在掉入一个空白。dynamic‘工作’的原因是因为它强制 Obj-C 链接所有东西,所以它总是动态的。

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

不能/不应覆盖扩展。

在扩展中覆盖功能(如属性或方法)是不可能的,正如苹果的 Swift Guide 中所记录的那样。

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

快速开发指南

为了与 Objective-C 兼容,编译器允许您在扩展中重写。但这实际上违反了语言指令。

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

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

所以分组没有错,但是应该在类中而不是在扩展中重写。

指示说明

如果方法是 Objective-C 兼容的,那么只能在子类的扩展中使用超类方法 override,即 load() initialize()

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

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

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

关于 dynamic修饰符

苹果开发者图书馆 (Archive.org)

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

当 Objective-C 运行时导入 Swift API 时,不能保证属性、方法、下标或初始化克劳斯·福尔曼。Swift 编译器仍然可以通过虚拟化或内联成员访问来优化代码的性能,绕过 Objective-C 运行时。

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

这就是为什么编译器允许您使用 overridedynamic

有一种方法可以实现类签名和实现(在扩展中)的清晰分离,同时维护在子类中进行重写的能力。诀窍是用变量代替函数

如果确保在单独的快速源文件中定义每个子类,则可以使用计算变量进行重写,同时将相应的实现清晰地组织在扩展中。这将绕过 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"

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

如果你搜索“迅速部分类”,你会发现各种链接,其中 Swift 的追随者说,Swift 不需要部分类,因为你可以使用扩展。有趣的是,如果你在 Google 搜索框中输入“迅速扩展”,它的第一个搜索建议是“迅速扩展覆盖”,而目前这个 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 的问题,但是我希望这个简单的解决方案可以帮助那些希望将方法从主源文件转移到扩展文件并遇到无覆盖问题的人。

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

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

Objective-C 运行时不能保证使用哪个扩展,正如 Apple 给你所描述的:

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

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