我倾向于只把必需的东西(存储的属性、初始化器)放入我的类定义中,而把其他的东西都移到它们自己的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
。这对我来说没有什么意义。我读到过静态调度Protocol Extensions的文章,但这不是一个协议。这是一个常规类,我希望在运行时动态调度方法调用。显然,对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 A
。dynamic
又解决了我的问题。
理论上,我可以把dynamic
加到我做过的所有override
上,但我觉得我在这里做错了什么。
像我这样使用extension
s对代码进行分组真的是错误的吗?
6条答案
按热度按时间tp5buhyn1#
扩展不能/不应重写。
如Apple的Swift指南中所述,不可能覆盖扩展中的功能(如属性或方法)。
扩展可以向类型添加新功能,但不能重写现有功能。
Swift Developer Guide
编译器允许您在扩展中重写以与Objective-C兼容。但实际上它违反了语言指令。
😊这让我想起了艾萨克·阿西莫夫的“Three Laws of Robotics“🤖
扩展(syntactic sugar)定义了接收自己参数的独立方法。例如,
layoutSubviews
所调用的函数取决于编译器在编译代码时所知道的上下文。UIView继承自UIResponder,而UIResponder继承自NSObject *,因此允许扩展中的覆盖,但不应为 *。所以分组没有什么错,但是你应该在类中而不是在扩展中重写。
指令注解
如果方法与Objective-C兼容,则只能对超类方法(即子类扩展中的
load()
initialize()
)执行override
操作。因此,我们可以看看为什么它允许您使用
layoutSubviews
进行编译。所有Swift应用程序都在Objective-C运行时内执行,除非使用纯Swift框架,该框架允许使用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运行时。这就是为什么编译器允许您使用
override
和dynamic
。4xy9mtcn2#
Swift的目标之一是静态调度,或者更确切地说是减少动态调度。然而Obj-C是一种非常动态的语言。你所看到的情况是由两种语言之间的联系和它们一起工作的方式所证实的。它不应该真正编译。
关于扩展的一个要点是,它们是用于扩展的,而不是用于替换/覆盖的。从名称和文档中都可以清楚地看出这是其意图。实际上,如果你从代码中去掉到Obj-C的链接(删除
NSObject
作为超类),它将无法编译。因此,编译器试图决定哪些可以静态调度,哪些必须动态调度,但由于代码中的Obj-C链接,它陷入了一个缺口。
dynamic
“工作”的原因是因为它强制Obj-C链接所有内容,所以它总是动态的。因此,使用扩展进行分组是没有错的,这很好,但是在扩展中重写是错误的。任何重写都应该在主类本身中,并且调用扩展点。
0qx6xfy63#
有一种方法可以实现类签名和实现(在扩展中)的清晰分离,同时保持子类中具有覆盖的能力。
如果你确保在一个单独的swift源文件中定义每个子类,你可以使用计算变量进行覆盖,同时保持相应的实现在扩展中的清晰组织。这将绕过Swift的“规则”,并将你的类的API/签名整齐地组织在一个地方:
...
...
每个类的扩展都可以使用相同的方法名来实现,因为它们是私有的,并且彼此不可见(只要它们在单独的文件中)。
如您所见,继承(使用变量名)使用super.variablename可以正常工作
vlju58qv4#
这个回答不是针对OP的,除了我觉得受到启发的事实以外,他的回答是“我倾向于只把必需品(存储的属性,初始化器)到我的类定义中,并将其他所有内容移动到它们自己的扩展中..."。我主要是一个C#程序员,在C#中,可以使用分部类来实现此目的。例如,Visual Studio使用分部类将与UI相关的内容放置在单独的源文件中,并使主源文件保持整洁,这样就不会让您分心。
如果你搜索“swift partial class”,你会发现很多链接,Swift的追随者说Swift不需要partial class,因为你可以使用扩展。有趣的是,如果你在Google搜索栏中输入“swift extension”,它的第一个搜索建议是“swift extension override”。目前这个堆栈溢出问题是第一个问题。我认为这意味着(缺乏)覆盖能力是搜索最多的与Swift扩展相关的主题,并强调了Swift扩展不可能替换部分类的事实,至少如果在编程中使用派生类的话。
总之,为了缩短冗长的介绍,我遇到了这个问题,当时我想将一些样板/包袱方法从我的C#-to-Swift程序生成的Swift类的主源文件中移走。在遇到将这些方法移到扩展后不允许覆盖这些方法的问题后,最后我实现了以下简单的解决方案:Swift主源文件仍然包含一些小的存根方法,这些方法调用扩展文件中的真实的方法,并且这些扩展方法被赋予唯一的名称以避免重写问题。
。
。
。
。
正如我在介绍中所说的,这并没有真正回答OP的问题,但我希望这个简单的解决方法可能对那些希望将方法从主源文件移动到扩展文件并遇到无覆盖问题的人有所帮助。
wkyowqbh5#
使用POP(面向协议的编程)覆盖扩展中的函数。
agxfikkp6#
只是想补充一下,对于Objective-C类,两个不同的类别可能最终会覆盖同一个方法,在这种情况下......嗯......可能会发生意想不到的事情。
Objective-C运行时并不保证将使用哪个扩展,正如Apple在此处所描述的:
如果在类别中声明的方法的名称与原始类中的方法或同一类的另一个类别中的方法相同(或者甚至是超类),那么在运行时使用哪个方法实现的行为是不确定的。但在使用类别向标准可可或CocoaTouch类添加方法时可能会出现问题。
Swift禁止纯Swift类这样做是一件好事,因为这种过于动态的行为是一个潜在的难以检测和调查bug的来源。