来自 C++/Java/C# 背景,我期待在 Swift 中看到虚拟方法,但是阅读 swift 文档我没有看到虚拟方法的提及。
我错过了什么?
由于浏览量大,我决定奖励最新且非常清晰/详细的答案。
来自 C++/Java/C# 背景,我期待在 Swift 中看到虚拟方法,但是阅读 swift 文档我没有看到虚拟方法的提及。
我错过了什么?
由于浏览量大,我决定奖励最新且非常清晰/详细的答案。
与 C++ 不同,在 Swift 中没有必要指定方法是虚拟的。编译器将计算出使用以下哪个:
(性能指标当然取决于硬件)
Objective-C 当然总是使用后者。4.9ns 的开销通常不是问题,因为这只是整个方法执行时间的一小部分。但是,在必要时,开发人员可以无缝回退到 C 或 C++。然而,在 Swift 中,编译器将分析可以使用哪个最快的并尝试代表您做出决定,支持内联、静态和虚拟,但保留用于 Objective-C 互操作性的消息传递。可以用标记方法dynamic
来鼓励消息传递。
这样做的一个副作用是,动态调度提供的一些强大功能可能不可用,而以前可能假设任何 Objective-C 方法都是如此。动态分派用于方法拦截,而方法拦截又由以下人员使用:
上述各种特性是一种late binding
语言所提供的特性。请注意,虽然 Java 使用 vtable dispatch 进行方法调用,但它仍然被认为是一种后期绑定语言,因此凭借虚拟机和类加载器系统(这是提供运行时检测的另一种方法)能够实现上述功能。“纯”Swift(没有 Objective-C 互操作)就像 C++ 一样,是一种具有静态调度的直接可执行编译语言,因此这些动态特性在运行时是不可能的。在 ARC 的传统中,我们可能会看到更多此类功能转向编译时间,这在“每瓦性能”方面具有优势——这是移动计算中的一个重要考虑因素。
所有方法都是虚拟的;但是,您需要声明您正在使用override
关键字覆盖基类中的方法:
来自Swift 编程指南:
覆盖
子类可以提供它自己的实例方法、类方法、实例属性或下标的自定义实现,否则它会从超类继承。这称为覆盖。
要覆盖原本会被继承的特性,请在覆盖定义前加上
override
关键字。这样做表明您打算提供覆盖并且没有错误地提供匹配的定义。意外覆盖可能会导致意外行为,并且override
在编译代码时,任何没有关键字的覆盖都会被诊断为错误。该
override
关键字还提示 Swift 编译器检查覆盖类的超类(或其父类之一)是否具有与您为覆盖提供的声明相匹配的声明。此检查可确保您的覆盖定义是正确的。
class A {
func visit(target: Target) {
target.method(self);
}
}
class B: A {}
class C: A {
override func visit(target: Target) {
target.method(self);
}
}
class Target {
func method(argument: A) {
println("A");
}
func method(argument: B) {
println("B");
}
func method(argument: C) {
println("C");
}
}
let t = Target();
let a: A = A();
let ab: A = B();
let b: B = B();
let ac: A = C();
let c: C = C();
a.visit(t);
ab.visit(t);
b.visit(t);
ac.visit(t);
c.visit(t);
请注意of和self
中的引用。就像在 Java 中一样,它不会被复制,而是保持相同的类型,直到它再次用于覆盖。visit()
A
C
self
结果是A, A, A, C, C所以没有可用的动态调度。很遗憾。
让我们从定义动态调度开始。
动态调度被认为是面向对象语言的主要特征。根据维基百科,这是选择在运行时调用哪个多态操作(方法/函数)实现的过程。我强调运行时间是有原因的,因为这是它与静态调度的区别。使用静态调度,对方法的调用在编译时被解析。在 C++ 中,这是调度的默认形式。对于动态调度,方法必须声明为.virtual
现在让我们检查一下什么是虚函数以及它在 C++ 上下文中的行为方式
在 C++ 中,虚函数是在基类中声明并被派生类覆盖的成员函数。它的主要特点是,如果我们有一个在基类中声明为 virtual 的函数,并且在派生类中定义了相同的函数,那么派生类中的函数会被派生类的对象调用,即使它是使用引用基类。
考虑这个例子,取自这里:
class Animal
{
public:
virtual void eat() { std::cout << "I'm eating generic food."; }
};
class Cat : public Animal
{
public:
void eat() { std::cout << "I'm eating a rat."; }
};
如果我们调用eat()
一个Cat
对象,但我们使用指向 的指针Animal
,则输出将是“<em>我正在吃一只老鼠。”</p>
现在我们可以研究这一切如何在 Swift 中发挥作用。我们有四种调度类型,如下(从最快到最慢):
让我们仔细看看动态调度。作为初步,您必须了解值类型和引用类型之间的区别。为了使这个答案保持合理的长度,我们只说如果一个实例是一个值类型,它会保留其数据的唯一副本。如果它是一个引用类型,它与所有其他实例共享一个数据副本。值类型和引用类型都支持静态调度。
但是,对于动态调度,您需要一个引用类型。原因是动态调度需要继承,而值类型不支持的继承需要引用类型。
如何在 Swift 中实现动态调度?有两种方法可以做到这一点。第一种是使用继承:子类化一个基类,然后覆盖基类的一个方法。我们之前的 C++ 示例在 Swift 中看起来像这样:
class Animal {
init() {
print("Animal created.")
}
func eat() {
print("I'm eating generic food.")
}
}
class Cat: Animal {
override init() {
print("Cat created.")
}
override func eat() {
print("I'm eating a rat.")
}
}
如果您现在运行以下代码:
let cat = Cat()
cat.eat()
控制台输出将是:
Cat created.
Animal created.
I'm eating a rat.
如您所见,无需将基类方法标记为virtual
,编译器会自动决定使用哪个调度选项。
第二种实现动态调度的方法是使用dynamic
关键字和@objc
前缀。我们需要@objc
将我们的方法暴露给 Objective-C 运行时,它完全依赖于动态调度。然而,Swift 只有在别无选择的情况下才会使用它。如果编译器可以在编译时决定使用哪个实现,它会选择退出动态调度。我们可能会使用@objc dynamic
Key -Value Observing或method swizzling,这两者都超出了这个答案的范围。
从 Xcode 8.xx 和 9 Beta 开始,C++ 中的虚拟方法可能会像这样在 Swift 3 和 4 中翻译:
protocol Animal: AnyObject { // as a base class in C++; class-only protocol in Swift
func hello()
}
extension Animal { // implementations of the base class
func hello() {
print("Zzz..")
}
}
class Dog: Animal { // derived class with a virtual function in C++
func hello() {
print("Bark!")
}
}
class Cat: Animal { // another derived class with a virtual function in C++
func hello() {
print("Meow!")
}
}
class Snoopy: Animal { // another derived class with no such a function
//
}
试试看。
func test_A() {
let array = [Dog(), Cat(), Snoopy()] as [Animal]
array.forEach() {
$0.hello()
}
// Bark!
// Meow!
// Zzz..
}
func sayHello<T: Animal>(_ x: T) {
x.hello()
}
func test_B() {
sayHello(Dog())
sayHello(Cat())
sayHello(Snoopy())
// Bark!
// Meow!
// Zzz..
}
总之,我认为,我们在 C++ 中所做的类似事情可以通过 Swift 中的Protocol和Generic来实现。
我也来自 C++ 世界并面临同样的问题。以上似乎可行,但它看起来像是一种 C++ 方式,而不是某种 Swifty 方式。
欢迎任何进一步的建议!
Swift 对于 Objective-C 程序员来说很容易学习,在 Objective-C 中没有虚拟方法,至少不是你想象的那样。如果您在 SO 上寻找有关如何在 Objective-C 中创建抽象类或虚拟方法的说明,通常它是一种普通方法,只会引发异常并使应用程序崩溃。(这有点道理,因为你不应该调用虚拟方法)
因此,如果 Swift 文档没有提到虚拟方法,我的猜测是,就像在 Objective-C 中一样,没有。