Swift: Overriding Self-requirement 是允许的,但会导致运行时错误。为什么?

标签 swift runtime-error type-safety typesafe

我刚开始学习 Swift(v.2.x),因为我很好奇新功能是如何发挥作用的,尤其是带有 self 要求的协议(protocol)

下面的示例将编译得很好,但会导致任意运行时影响的发生:

// 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:

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

  • Self 只是它出现的完整类名的语法快捷方式,可以用后者代替,而不会改变含义。但是,它不能与它出现在协议(protocol)定义中时具有相同的含义。

  • Self 是一种通用/关联类型(在协议(protocol)和类中),在派生/采用类中重新实例化。如果是这种情况,编译器应该拒绝覆盖 SuperMario 中的 getFriend

也许真正的定义两者都不是。如果有更多语言经验的人可以阐明该主题,那就太好了。

最佳答案

是的,好像有点矛盾。 Self 关键字,当用作返回类型时,显然意味着“self 作为 Self 的一个实例”。例如,给定这个协议(protocol)

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 的精确语义的任何选择都不会改变事情,您的代码说明了编译器很容易被愚弄的情况之一,它能做的最好的事情就是生成代码来推迟检查到运行时。在这种情况下,应该委托(delegate)给运行时的检查与转换有关,在我看来,您的示例揭示的一个有趣的方面是,在特定位置 Swift 似乎委托(delegate)任何东西,因此,不可避免的崩溃比它应该的更戏剧化。

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( ).字节码中显式 checkcast 指令的存在很容易证实这一点。

相反,据我目前所知,Swift 不会在调用前尝试任何转换(在编译代码中没有调用转换例程),因此可怕的、 Uncaught Error 是唯一可能的结果。相反,如果您的代码产生了运行时“无法转换”错误消息,或者类似的错误消息,我会对语言的行为完全满意。

关于Swift: Overriding Self-requirement 是允许的,但会导致运行时错误。为什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35270120/

相关文章:

enums - 在 Swift 中,是否可以在编译时为枚举的关联值预填充值?

ios - 返回时出现 Swift ios 错误

java - map 中的动态模板依赖

java - 在java中调用webservice时出现"org.xml.sax.SAXException: SimpleDeserializer encountered a child element, which is NOT expected..."错误

java - 使用 Java 泛型确保类型安全

scala - 使用类型为编译时检查的任意约束建模

ios - 错误 : UICollectionView received layout attributes for a cell with an index path that does not exist in Swift

ios - uitableview 中的 Uilabel 在调试模拟器颜色混合层中不显示不透明而不透明

Python - 从 C++ DLL 错误打印值

c++ - Visual C++ 10 运行时出现奇怪的异常