【发布时间】:2014-07-23 17:20:37
【问题描述】:
来自 C++/Java/C# 背景,我期待在 Swift 中看到虚拟方法,但是阅读 swift 文档后我没有看到虚拟方法的提及。
我错过了什么?
由于浏览量大,我决定奖励最新且非常清晰/详细的答案。
【问题讨论】:
标签: virtual-functions swift dynamic-dispatch
来自 C++/Java/C# 背景,我期待在 Swift 中看到虚拟方法,但是阅读 swift 文档后我没有看到虚拟方法的提及。
我错过了什么?
由于浏览量大,我决定奖励最新且非常清晰/详细的答案。
【问题讨论】:
标签: virtual-functions swift dynamic-dispatch
Swift 对于 Objective-C 程序员来说很容易学习,而且在 Objective-C 中没有虚拟方法,至少不是你想象的那样。如果您在 SO 上寻找有关如何在 Objective-C 中创建抽象类或虚拟方法的说明,通常它是一种普通方法,只会引发异常并使应用程序崩溃。 (这有点道理,因为你不应该调用虚拟方法)
因此,如果 Swift 文档没有提及虚拟方法,我的猜测是,就像在 Objective-C 中一样,没有。
【讨论】:
所有方法都是虚的;但是您需要声明您正在使用 override 关键字覆盖基类中的方法:
覆盖
子类可以提供自己的实例的自定义实现 方法、类方法、实例属性或下标 否则从超类继承。这称为覆盖。
要覆盖原本会被继承的特征,您 使用
override关键字作为覆盖定义的前缀。这样做 阐明您打算提供覆盖但未提供 错误的匹配定义。意外超车可能会导致 意外行为,以及没有override关键字的任何覆盖 编译代码时被诊断为错误。
override关键字还会提示 Swift 编译器检查 你的重写类的超类(或其父类之一)有一个 与您为覆盖提供的声明相匹配的声明。这 检查确保您的覆盖定义是正确的。
【讨论】:
virtual。看起来 Swift 从 C# 中借了很多东西;例如泛型类型上的 where 关键字。
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);
注意A 和C 的visit() 中的self 引用。就像在 Java 中一样,它不会被复制,而是 self 保持相同的类型,直到它再次用于覆盖。
结果是 A, A, A, C, C,因此没有可用的动态调度。很遗憾。
【讨论】:
与 C++ 不同,在 Swift 中没有必要指定方法是虚拟的。编译器将确定使用以下哪一项:
(性能指标当然取决于硬件)
Objective-C 当然总是使用后者。 4.9ns 的开销通常不是问题,因为这只是整个方法执行时间的一小部分。但是,在必要时,开发人员可以无缝回退到 C 或 C++。然而,在 Swift 中,编译器将分析可以使用哪个最快的并尝试代表您做出决定,支持内联、静态和虚拟,但保留用于 Objective-C 互操作性的消息传递。可以使用dynamic 标记方法以鼓励消息传递。
这样做的一个副作用是,动态分派提供的一些强大功能可能不可用,而以前可能假设任何 Objective-C 方法都是如此。动态分派用于方法拦截,依次被:
上述各种功能是由late binding 语言提供的。请注意,虽然 Java 使用 vtable dispatch 进行方法调用,但它仍然被认为是一种后期绑定语言,因此凭借虚拟机和类加载器系统(这是提供运行时检测的另一种方法)能够实现上述功能。 “纯”Swift(没有 Objective-C 互操作)就像 C++ 一样,是一种具有静态调度的直接可执行编译语言,那么这些动态特性在运行时是不可能的。在 ARC 的传统中,我们可能会看到更多此类功能转向编译时间,这在“每瓦性能”方面具有优势——这是移动计算中的一个重要考虑因素。
【讨论】:
@objc 指令,则使用消息传递,尽管在这些情况下可能仍然使用静态样式,我们可以通过将方法声明为 dynamic 来避免这种情况. .相当复杂。
从 Xcode 8.x.x 和 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 方式。
欢迎任何进一步的建议!
【讨论】:
让我们从定义动态调度开始。
动态调度被认为是面向对象语言的主要特征。
这是选择在运行时调用多态操作(方法/函数)的实现的过程,according to Wikipedia。
我强调运行时间是有原因的,因为这是它与 静态调度 的区别。使用静态调度,对方法的调用在编译时解决。在 C++ 中,这是调度的默认形式。对于动态调度,方法必须声明为virtual。
现在让我们来看看什么是虚函数以及它在 C++ 上下文中的行为
在 C++ 中,虚函数是在基类中声明并被派生类覆盖的成员函数。 它的主要特点是,如果我们有一个在基类中声明为 virtual 的函数,并且在派生类中定义了相同的函数,那么派生类中的函数会被派生类的对象调用,即使它是使用引用基类。
考虑这个例子,taken from here:
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."; }
};
如果我们在 Cat 对象上调用 eat(),但我们使用指向 Animal 的指针,则输出将是“我正在吃老鼠。”
现在我们可以研究这一切如何在 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,这两者都超出了此答案的范围。
【讨论】: