【问题标题】:Does Swift have dynamic dispatch and virtual methods?Swift 有动态调度和虚方法吗?
【发布时间】:2014-07-23 17:20:37
【问题描述】:

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

我错过了什么?


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

【问题讨论】:

    标签: virtual-functions swift dynamic-dispatch


    【解决方案1】:

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

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

    【讨论】:

    • 在 Swift 中不需要显式指定一个方法为 virtual,编译器将决定是否: a) 内联 b) 使用静态调度(非虚拟) b) 使用 vtable 调度(如 Java ) c) 使用像 Objective-C 这样的动态调度。 .许多 Objective-C 资深人士对 Swift 不默认动态调度感到惊讶。
    【解决方案2】:

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

    来自Swift Programming Guide

    覆盖

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

    要覆盖原本会被继承的特征,您 使用 override 关键字作为覆盖定义的前缀。这样做 阐明您打算提供覆盖但未提供 错误的匹配定义。意外超车可能会导致 意外行为,以及没有 override 关键字的任何覆盖 编译代码时被诊断为错误。

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

    【讨论】:

    • 这是像 C# 中的“虚拟”还是像 C# 中的“新”。例如。在运行时是变量的静态,还是用来决定调用什么方法的实例的动态类型?
    • @IanRingrose 这就像来自 C# 的 virtual。看起来 Swift 从 C# 中借了很多东西;例如泛型类型上的 where 关键字。
    【解决方案3】:
    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);
    

    注意ACvisit() 中的self 引用。就像在 Java 中一样,它不会被复制,而是 self 保持相同的类型,直到它再次用于覆盖。

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

    【讨论】:

    • 你能检查一下你的 AC 结果吗
    • Swift 会自动选择内联、静态分派、vtable 分派。为了允许动态调度需要扩展 NSObject 或添加 'objc' 指令,但是在最新版本中,我们必须将每个方法标记为动态的。 . .我希望动态是默认设置。
    • 您的示例表明 Swift 总是静态查找方法重载(顺便说一句,还有泛型)。动态调度在 Swift 中确实有效,但重载的方法是静态解析的。
    • 您的“ac”示例不是显示动态调度 is 实际上可用吗?既然定义为A型,但是Target类打印的时候输出C型?如果它打印出 A,那么它将是静态调度...
    【解决方案4】:

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

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

    • 内联方法:0 ns
    • 静态调度:
    • 虚拟分派 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 的传统中,我们可能会看到更多此类功能转向编译时间,这在“每瓦性能”方面具有优势——这是移动计算中的一个重要考虑因素。

    【讨论】:

    • 我想知道“纯 Swift”类是否会支持 KVO?
    • Cocoa 风格的 KVO 仅在类扩展 NSObject 时才有效。我认为 Swift 内置了一个 Observer 系统。 (还没试过)。
    • @nielsbot KVO 依赖于根据名称查找函数。那就是动态调度。因此,如果 Swift 使用 KVO,它必须使用动态调度。这再次意味着它没有使用 vtable 查找,这就是所谓的“纯”swift。我认为我们应该抛弃纯 swift 的概念。 ObjC 是 Swift 的自然组成部分。它的设计考虑了 ObjC。它是 Swift DNA 的一部分。
    • 在什么意义上“编译器会工作”?虚拟调度与静态调度在行为上有所不同,你有一些库,但你不知道将调用哪个方法,因为它取决于当前/以前编译器的结果?对我来说有点神奇——编译器可以决定性能,但不能决定行为。
    • @greenoldman 一般规则是:Swift 偏爱静态类型(内联、静态、vtable),并会选择其中哪些是合适的,内联用于小型主体,如果没有子类则为静态,否则为 vtable .同时,如果类扩展了 NSObject 或具有 @objc 指令,则使用消息传递,尽管在这些情况下可能仍然使用静态样式,我们可以通过将方法声明为 dynamic 来避免这种情况. .相当复杂。
    【解决方案5】:

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

    我也来自 C++ 世界并面临同样的问题。上面的方法似乎有效,但它看起来像 C++ 方式,但不是 Swifty 方式。

    欢迎任何进一步的建议!

    【讨论】:

    • 仅供参考。最好史努比应该能够扩展狗。但是动态调度在那一点上都崩溃了。由于动态调度将默认为协议而不是泛型类型。史努比伸出狗的那一刻,他又开始吠叫了?
    【解决方案6】:

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

    动态调度被认为是面向对象语言的主要特征。 这是选择在运行时调用多态操作(方法/函数)的实现的过程,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 中发挥作用。 我们有四种调度类型,如下(从最快到最慢):

    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 dynamic 用于Key-Value Observingmethod swizzling,这两者都超出了此答案的范围。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-06-09
      • 1970-01-01
      • 1970-01-01
      • 2013-07-11
      • 1970-01-01
      • 1970-01-01
      • 2011-10-31
      • 1970-01-01
      相关资源
      最近更新 更多