Swift: 允许重写自我要求,但会导致运行时错误。为什么?
Swift: Overriding Self-requirement is allowed, but causes runtime error. Why?
我刚开始学习 Swift (v. 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!或者更确切地说,'instead of a runtime exception',因为这个方法甚至没有在调用它的对象上定义。)
这是否证明 Swift 目前不是类型安全的?
编辑#1:
像这样不可预测的行为是不能接受的。
真正的问题是,关键字 Self
(首字母大写)的确切含义是什么。
我在网上找不到任何东西,但至少有这两种可能性:
Self
只是它出现的完整 class 名称的语法快捷方式,它可以被后者替换而不会改变含义。但是,它不能与它出现在协议定义中时具有相同的含义。
Self
是一种 generic/associated 类型(在协议和 classes 中),在 deriving/adopting classes。如果是这种情况,编译器应该拒绝覆盖 SuperMario
.
中的 getFriend
也许真正的定义两者都不是。如果有更多语言经验的人可以阐明该主题,那就太好了。
这里的问题好像是违约:
您定义 getFriend()
到 return 一个接收者实例(Self)。这里的问题是 SuperMario
不是 return self
但它 return 是 SuperMario
.
类型的新实例
现在,当 FireFlowerMario
继承该方法时,合同规定该方法应该 return 为 FireFlowerMario
,但继承的方法 return 为 SuperMario
!然后将此实例视为 FireFlowerMario
,特别是:Swift 尝试访问 SuperMario
上不存在的实例变量 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()
}
}
为什么编译器允许它?很难捕捉到这样的东西,我猜。对于 SuperMario
,合同仍然有效:方法 getFriend
执行 return 相同 class 的实例。当您创建 subclass FireFlowerMario
时,合同会中断:编译器是否应该注意到 superclass 可能违反合同?这将是一项昂贵的检查,可能更适合静态分析器,恕我直言(另外,如果编译器无法访问 SuperMario
的源代码会发生什么?如果 class 来自图书馆?)
所以实际上SuperMario
有责任确保合同在subclass时仍然有效。
对,好像有点矛盾。当用作 return 类型时,Self 关键字显然表示 'self as an instance of Self'。例如,给定此协议
protocol ReturnsReceived {
/// Returns other.
func doReturn(other: Self) -> Self
}
我们无法实现如下
class Return: ReturnsReceived {
func doReturn(other: Return) -> Self {
return other // Error
}
}
因为我们得到一个编译器错误 ("Cannot convert return expression of type 'Return' to return type 'Self'"),如果我们违反 doReturn() 的约定并且 return 自己而不是其他,它就会消失。而且我们不能写
class Return: ReturnsReceived {
func doReturn(other: Return) -> Return { // Error
return other
}
}
因为这只允许在最终 class 中使用,即使 Swift 支持协变 return 类型。 (以下实际编译。)
final class Return: ReturnsReceived {
func doReturn(other: Return) -> Return {
return other
}
}
另一方面,正如您所指出的,Return 的子class 可以'override' 自我要求并愉快地履行 ReturnsReceived 的合同,就好像 Self 是符合 class' 名称的简单占位符。
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
}
}
我可能是错的,但我认为:
if Self as a return 类型真的意味着'self as an instance of
Self',编译器不应该接受这种Self要求
覆盖,因为它可以 return 个实例
不是自己;否则,
如果作为 return 类型的 Self 必须只是一个没有进一步含义的占位符,那么在我们的示例中,编译器应该已经允许覆盖 Return 中的 Self 要求class.
就是说,这里关于 Self 的精确语义的任何选择都不一定会改变事情,您的代码说明了编译器很容易被愚弄的情况之一,它能做的最好的事情就是生成代码以将检查推迟到 运行 时间。在这种情况下,应该委托给 运行time 的检查与转换有关,在我看来,您的示例揭示了一个有趣的方面,即在特定位置 Swift 似乎 不委托任何东西,因此不可避免的崩溃比它应该的更戏剧化。
Swift 能够在 运行 时间检查转换。让我们考虑以下代码。
let sm = SuperMario()
let ffm = sm as! FireFlowerMario
ffm.throwFireballs()
这里我们创建一个 SuperMario 并将其向下转换为 FireFlowerMario。这两个 class 并非无关紧要,我们向编译器保证(as!)我们知道我们在做什么,因此编译器保持原样并顺利编译第二行和第三行。但是,程序在 运行 时失败,抱怨它
Could not cast value of type
'SomeModule.SuperMario' (0x...) to
'SomeModule.FireFlowerMario' (0x...).
在第二行中尝试转换时。这不是错误或令人惊讶的行为。例如,Java 会执行完全相同的操作:编译代码,并在 运行 时失败并抛出 ClassCastException。重要的是应用程序在 运行 时间可靠地崩溃。
你的代码是一种更复杂的欺骗编译器的方法,但它归结为同样的问题:有一个 SuperMario 而不是 FireFlowerMario。不同之处在于,在您的情况下,我们没有收到温和的 "could not cast" 消息,但在真正的 Xcode 项目中,调用 throwFireballs() 时会出现突然且严重的错误。
在同样的情况下,Java 失败(在 运行 时间)并出现我们在上面看到的相同错误(ClassCastException),这意味着它在调用之前尝试转换(到 FireFlowerMario)由 queryFriend() 编辑的对象 return 上的 throwFireballs()。字节码中显式 checkcast 指令的存在很容易证实这一点。
Swift 相反,据我目前所见,在调用之前没有尝试任何转换(编译代码中没有调用转换例程),所以一个可怕的,未捕获的错误是唯一可能的结果。相反,如果您的代码产生了 运行 次 "could not cast" 错误消息,或者类似的错误消息,我会对语言的行为完全满意。
我刚开始学习 Swift (v. 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!或者更确切地说,'instead of a runtime exception',因为这个方法甚至没有在调用它的对象上定义。)
这是否证明 Swift 目前不是类型安全的?
编辑#1:
像这样不可预测的行为是不能接受的。
真正的问题是,关键字 Self
(首字母大写)的确切含义是什么。
我在网上找不到任何东西,但至少有这两种可能性:
Self
只是它出现的完整 class 名称的语法快捷方式,它可以被后者替换而不会改变含义。但是,它不能与它出现在协议定义中时具有相同的含义。
中的Self
是一种 generic/associated 类型(在协议和 classes 中),在 deriving/adopting classes。如果是这种情况,编译器应该拒绝覆盖SuperMario
.getFriend
也许真正的定义两者都不是。如果有更多语言经验的人可以阐明该主题,那就太好了。
这里的问题好像是违约:
您定义 getFriend()
到 return 一个接收者实例(Self)。这里的问题是 SuperMario
不是 return self
但它 return 是 SuperMario
.
现在,当 FireFlowerMario
继承该方法时,合同规定该方法应该 return 为 FireFlowerMario
,但继承的方法 return 为 SuperMario
!然后将此实例视为 FireFlowerMario
,特别是:Swift 尝试访问 SuperMario
上不存在的实例变量 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()
}
}
为什么编译器允许它?很难捕捉到这样的东西,我猜。对于 SuperMario
,合同仍然有效:方法 getFriend
执行 return 相同 class 的实例。当您创建 subclass FireFlowerMario
时,合同会中断:编译器是否应该注意到 superclass 可能违反合同?这将是一项昂贵的检查,可能更适合静态分析器,恕我直言(另外,如果编译器无法访问 SuperMario
的源代码会发生什么?如果 class 来自图书馆?)
所以实际上SuperMario
有责任确保合同在subclass时仍然有效。
对,好像有点矛盾。当用作 return 类型时,Self 关键字显然表示 'self as an instance of Self'。例如,给定此协议
protocol ReturnsReceived {
/// Returns other.
func doReturn(other: Self) -> Self
}
我们无法实现如下
class Return: ReturnsReceived {
func doReturn(other: Return) -> Self {
return other // Error
}
}
因为我们得到一个编译器错误 ("Cannot convert return expression of type 'Return' to return type 'Self'"),如果我们违反 doReturn() 的约定并且 return 自己而不是其他,它就会消失。而且我们不能写
class Return: ReturnsReceived {
func doReturn(other: Return) -> Return { // Error
return other
}
}
因为这只允许在最终 class 中使用,即使 Swift 支持协变 return 类型。 (以下实际编译。)
final class Return: ReturnsReceived {
func doReturn(other: Return) -> Return {
return other
}
}
另一方面,正如您所指出的,Return 的子class 可以'override' 自我要求并愉快地履行 ReturnsReceived 的合同,就好像 Self 是符合 class' 名称的简单占位符。
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
}
}
我可能是错的,但我认为:
if Self as a return 类型真的意味着'self as an instance of Self',编译器不应该接受这种Self要求 覆盖,因为它可以 return 个实例 不是自己;否则,
如果作为 return 类型的 Self 必须只是一个没有进一步含义的占位符,那么在我们的示例中,编译器应该已经允许覆盖 Return 中的 Self 要求class.
就是说,这里关于 Self 的精确语义的任何选择都不一定会改变事情,您的代码说明了编译器很容易被愚弄的情况之一,它能做的最好的事情就是生成代码以将检查推迟到 运行 时间。在这种情况下,应该委托给 运行time 的检查与转换有关,在我看来,您的示例揭示了一个有趣的方面,即在特定位置 Swift 似乎 不委托任何东西,因此不可避免的崩溃比它应该的更戏剧化。
Swift 能够在 运行 时间检查转换。让我们考虑以下代码。
let sm = SuperMario()
let ffm = sm as! FireFlowerMario
ffm.throwFireballs()
这里我们创建一个 SuperMario 并将其向下转换为 FireFlowerMario。这两个 class 并非无关紧要,我们向编译器保证(as!)我们知道我们在做什么,因此编译器保持原样并顺利编译第二行和第三行。但是,程序在 运行 时失败,抱怨它
Could not cast value of type
'SomeModule.SuperMario' (0x...) to
'SomeModule.FireFlowerMario' (0x...).
在第二行中尝试转换时。这不是错误或令人惊讶的行为。例如,Java 会执行完全相同的操作:编译代码,并在 运行 时失败并抛出 ClassCastException。重要的是应用程序在 运行 时间可靠地崩溃。
你的代码是一种更复杂的欺骗编译器的方法,但它归结为同样的问题:有一个 SuperMario 而不是 FireFlowerMario。不同之处在于,在您的情况下,我们没有收到温和的 "could not cast" 消息,但在真正的 Xcode 项目中,调用 throwFireballs() 时会出现突然且严重的错误。
在同样的情况下,Java 失败(在 运行 时间)并出现我们在上面看到的相同错误(ClassCastException),这意味着它在调用之前尝试转换(到 FireFlowerMario)由 queryFriend() 编辑的对象 return 上的 throwFireballs()。字节码中显式 checkcast 指令的存在很容易证实这一点。
Swift 相反,据我目前所见,在调用之前没有尝试任何转换(编译代码中没有调用转换例程),所以一个可怕的,未捕获的错误是唯一可能的结果。相反,如果您的代码产生了 运行 次 "could not cast" 错误消息,或者类似的错误消息,我会对语言的行为完全满意。