27

来自 C++/Java/C# 背景,我期待在 Swift 中看到虚拟方法,但是阅读 swift 文档我没有看到虚拟方法的提及。

我错过了什么?


由于浏览量大,我决定奖励最新且非常清晰/详细的答案。

4

6 回答 6

21

与 C++ 不同,在 Swift 中没有必要指定方法是虚拟的。编译器将计算出使用以下哪个:

(性能指标当然取决于硬件)

  • 内联方法:0 ns
  • 静态调度:< 1.1ns
  • 虚拟调度 1.1ns(如指定的 Java、C# 或 C++)。
  • 动态调度 4.9ns(如 Objective-C)。

Objective-C 当然总是使用后者。4.9ns 的开销通常不是问题,因为这只是整个方法执行时间的一小部分。但是,在必要时,开发人员可以无缝回退到 C 或 C++。然而,在 Swift 中,编译器将分析可以使用哪个最快的并尝试代表您做出决定,支持内联、静态和虚拟,但保留用于 Objective-C 互操作性的消息传递。可以用标记方法dynamic来鼓励消息传递。

这样做的一个副作用是,动态调度提供的一些强大功能可能不可用,而以前可能假设任何 Objective-C 方法都是如此。动态分派用于方法拦截,而方法拦截又由以下人员使用:

  • Cocoa 风格的属性观察者。
  • CoreData 模型对象检测。
  • 面向方面的编程

上述各种特性是一种late binding语言所提供的特性。请注意,虽然 Java 使用 vtable dispatch 进行方法调用,但它仍然被认为是一种后期绑定语言,因此凭借虚拟机和类加载器系统(这是提供运行时检测的另一种方法)能够实现上述功能。“纯”Swift(没有 Objective-C 互操作)就像 C++ 一样,是一种具有静态调度的直接可执行编译语言,因此这些动态特性在运行时是不可能的。在 ARC 的传统中,我们可能会看到更多此类功能转向编译时间,这在“每瓦性能”方面具有优势——这是移动计算中的一个重要考虑因素。

于 2014-08-22T01:23:46.420 回答
7

所有方法都是虚拟的;但是,您需要声明您正在使用override关键字覆盖基类中的方法:

来自Swift 编程指南

覆盖

子类可以提供它自己的实例方法、类方法、实例属性或下标的自定义实现,否则它会从超类继承。这称为覆盖。

要覆盖原本会被继承的特性,请在覆盖定义前加上override关键字。这样做表明您打算提供覆盖并且没有错误地提供匹配的定义。意外覆盖可能会导致意外行为,并且override在编译代码时,任何没有关键字的覆盖都会被诊断为错误。

override关键字还提示 Swift 编译器检查覆盖类的超类(或其父类之一)是否具有与您为覆盖提供的声明相匹配的声明。此检查可确保您的覆盖定义是正确的。

于 2014-06-03T11:52:53.523 回答
4
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()ACself

结果是A, A, A, C, C所以没有可用的动态调度。很遗憾。

于 2014-06-04T21:16:29.603 回答
2

让我们从定义动态调度开始。

动态调度被认为是面向对象语言的主要特征。根据维基百科,这是选择在运行时调用哪个多态操作(方法/函数)实现的过程。我强调运行时间是有原因的,因为这是它与静态调度的区别。使用静态调度,对方法的调用在编译时被解析。在 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 中发挥作用。我们有四种调度类型,如下(从最快到最慢):

  1. 在线调度
  2. 静态调度
  3. 虚拟调度
  4. 动态调度

让我们仔细看看动态调度。作为初步,您必须了解类型和引用类型之间的区别。为了使这个答案保持合理的长度,我们只说如果一个实例是一个值类型,它会保留其数据的唯一副本。如果它是一个引用类型,它与所有其他实例共享一个数据副本。值类型和引用类型都支持静态调度。

但是,对于动态调度,您需要一个引用类型。原因是动态调度需要继承,而值类型不支持的继承需要引用类型。

如何在 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 dynamicKey -Value Observingmethod swizzling,这两者都超出了这个答案的范围。

于 2021-03-10T16:43:57.770 回答
2

从 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 中的ProtocolGeneric来实现。

我也来自 C++ 世界并面临同样的问题。以上似乎可行,但它看起来像是一种 C++ 方式,而不是某种 Swifty 方式。

欢迎任何进一步的建议!

于 2017-06-24T08:49:43.573 回答
-2

Swift 对于 Objective-C 程序员来说很容易学习,在 Objective-C 中没有虚拟方法,至少不是你想象的那样。如果您在 SO 上寻找有关如何在 Objective-C 中创建抽象类或虚拟方法的说明,通常它是一种普通方法,只会引发异常并使应用程序崩溃。(这有点道理,因为你不应该调用虚拟方法)

因此,如果 Swift 文档没有提到虚拟方法,我的猜测是,就像在 Objective-C 中一样,没有。

于 2014-06-03T11:47:56.723 回答