代码之家  ›  专栏  ›  技术社区  ›  domin

Swift:允许覆盖自我要求,但会导致运行时错误。为什么?

  •  10
  • domin  · 技术社区  · 10 年前

    我刚开始学习Swift(第2.x版),因为我很好奇新功能如何发挥,尤其是 具有自我要求的协议 .

    下面的示例可以很好地编译,但会导致任意的运行时效果:

    // The protocol with Self requirement
    protocol Narcissistic {
        func getFriend() -> Self
    }
    
    
    // Base class that adopts the protocol
    class Mario : Narcissistic  {
        func getFriend() -> Self {
            print("Mario.getFriend()")
            return self;
        }
    }
    
    
    // Intermediate class that eliminates the
    // Self requirement by specifying an explicit type
    // (Why does the compiler allow this?)
    class SuperMario : Mario {
        override func getFriend() -> SuperMario {
            print("SuperMario.getFriend()")
            return SuperMario();
        }
    }
    
    // Most specific class that defines a field whose
    // (polymorphic) access will cause the world to explode
    class FireFlowerMario : SuperMario {
        let fireballCount = 42
        
        func throwFireballs() {
            print("Throwing " + String(fireballCount) + " fireballs!")
        }
    }
    
    
    // Global generic function restricted to the protocol
    func queryFriend<T : Narcissistic>(narcissistic: T) -> T {
        return narcissistic.getFriend()
    }
    
    
    // Sample client code
    
    // Instantiate the most specific class
    let m = FireFlowerMario()
    
    // The call to the generic function is verified to return
    // the same type that went in -- 'FireFlowerMario' in this case.
    // But in reality, the method returns a 'SuperMario' and the
    // call to 'throwFireballs' will cause arbitrary
    // things to happen at runtime.
    queryFriend(m).throwFireballs()
    

    您可以看到实际的示例 on the IBM Swift Sandbox here . 在我的浏览器中,输出如下:

    SuperMario.getFriend()
    Throwing 32 fireballs!
    

    (而不是42!或者更确切地说,“而不是运行时异常”,因为这个方法甚至没有在调用它的对象上定义。)

    这是否证明了Swift目前不是类型安全的?

    编辑#1:

    像这样不可预测的行为必须是不可接受的。 真正的问题是,关键字的确切含义是什么 Self (大写首字母)有。 我在网上找不到任何东西,但至少有以下两种可能:

    • 自己 是它所出现的完整类名的简单语法快捷方式,它可以用后者替换,而不会改变其含义。但是,当它出现在协议定义中时,它不能具有相同的含义。

    • 自己 是一种泛型/关联类型(在协议和类中),在派生/采用类时被重新实例化。如果是这种情况,编译器应该拒绝重写 getFriend 在里面 SuperMario .

    也许这两者都不是真正的定义。如果有更多语言经验的人能对这个话题有所了解,那就太好了。

    2 回复  |  直到 6 年前
        1
  •  6
  •   Alberto Doda    10 年前

    是的,似乎有矛盾。Self关键字用作返回类型时,显然意味着“Self作为Self的实例”。例如,给定此协议

    protocol ReturnsReceived {
    
        /// Returns other.
        func doReturn(other: Self) -> Self
    }
    

    我们不能按如下方式实现它

    class Return: ReturnsReceived {
    
        func doReturn(other: Return) -> Self {
            return other    // Error
        }
    }
    

    因为我们得到一个编译器错误(“无法将‘return’类型的返回表达式转换为‘Self’类型”),如果我们违反doReturn()的约定并返回Self而不是other,则该错误将消失。我们不会写字

    class Return: ReturnsReceived {
    
        func doReturn(other: Return) -> Return {    // Error
            return other
        }
    }
    

    因为这仅在最终类中允许,即使Swift支持协变返回类型。(以下内容实际编译。)

    final class Return: ReturnsReceived {
    
        func doReturn(other: Return) -> Return {
            return other
        }
    }
    

    另一方面,正如您所指出的,Return的子类可以“覆盖”Self要求,并愉快地遵守ReturnsReceived的合同,就像Self是符合类名称的简单占位符一样。

    class SubReturn: Return {
    
        override func doReturn(other: Return) -> SubReturn {
            // Of course this crashes if other is not a
            // SubReturn instance, but let's ignore this
            // problem for now.
            return other as! SubReturn
        }
    }
    

    我可能错了,但我认为:

    • 如果Self作为返回类型真的意味着“Self作为 “Self”,编译器不应接受这种Self要求 重写,因为它可以返回 不是自我;否则

    • 如果Self作为返回类型必须只是一个占位符,没有进一步的含义,那么在我们的示例中,编译器应该已经允许重写return类中的Self要求。

    也就是说,在这里,关于Self的精确语义的任何选择都不一定会改变事情,您的代码说明了编译器很容易被欺骗的情况之一,它所能做的最好的事情就是生成代码,将检查推迟到运行时。在这种情况下,应该委托给运行时的检查与强制转换有关,在我看来,您的示例揭示的一个有趣的方面是,在特定的位置,Swift似乎 不 授权任何事情,因此不可避免的崩溃比应该发生的更为剧烈。

    Swift能够在运行时检查强制转换。让我们考虑下面的代码。

    let sm = SuperMario()
    let ffm = sm as! FireFlowerMario
    ffm.throwFireballs()
    

    在这里,我们创建一个SuperMario并将其向下转换为FireFlowerMario。这两个类并非无关,我们向编译器保证(as!)我们知道我们在做什么,所以编译器保持原样,并编译第二行和第三行。然而,程序在运行时失败,抱怨它

    Could not cast value of type
    'SomeModule.SuperMario' (0x...) to
    'SomeModule.FireFlowerMario' (0x...).
    

    在第二行尝试演员阵容时。这不是错误或令人惊讶的行为。例如,Java也会做同样的事情:编译代码,在运行时失败并出现ClassCastException。重要的是,应用程序在运行时可靠地崩溃。

    您的代码是欺骗编译器的一种更复杂的方法,但归根结底是同一个问题:有一个SuperMario而不是FireFlowerMario。不同的是,在您的情况下,我们不会收到温和的“无法投射”消息,但在真正的Xcode项目中,在调用throwFireballs()时会出现一个突然而可怕的错误。

    在相同的情况下,Java失败(在运行时),出现了我们在上面看到的相同错误(ClassCastException),这意味着它在对queryFriend()返回的对象调用throwFireballs()之前,会尝试转换(到FireFlowerMario)。字节码中存在显式的checkcast指令很容易证实这一点。

    相反,就我目前所见,Swift在调用之前不尝试任何强制转换(编译代码中没有调用任何强制转换例程),因此一个可怕的、未捕获的错误是唯一可能的结果。相反,如果您的代码生成了运行时“无法转换”错误消息,或类似的友好消息,我会对该语言的行为完全满意。

        2
  •  4
  •   DarkDust    10 年前

    这里的问题似乎违反了合同:

    您定义 getFriend() 返回receiver(Self)的实例。这里的问题是 SuperMario 不返回 self 但它返回类型为的新实例 超级马里奥 .

    现在,什么时候 FireFlowerMario 继承该方法,约定该方法应返回 FireFlower马里奥 但是,继承的方法返回 超级马里奥 ! 然后将此实例视为 FireFlower马里奥 ,特别是:Swift尝试访问实例变量 fireballCount 上不存在 超级马里奥 你反而会得到垃圾。

    您可以这样修复:

    class SuperMario : Mario {
        required override init() {
            super.init()
        }
    
        override func getFriend() -> SuperMario {
            print("SuperMario.getFriend()")
    
            // Dynamically create new instance of the same type as the receiver.
            let myClass = self.dynamicType
            return myClass.init()
        }
    }
    

    为什么编译器允许它?我想很难捕捉到这样的东西。对于 超级马里奥 ,合同仍然有效:方法 getFriend 不返回同一类的实例。当您创建子类时,合同会中断 FireFlower马里奥 :编译器是否应该注意到超类可能违反约定?这将是一个昂贵的检查,可能更适合静态分析器IMHO(另外,如果编译器没有访问 超级马里奥 的来源?如果该类来自图书馆,会发生什么?)

    所以实际上 超级马里奥 确保合同在子类化时仍然有效的责任。