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

dom*_*min 10 runtime-error type-safety typesafe swift

我刚刚开始学习Swift(v.2.x),因为我很好奇新功能是如何发挥作用的,尤其是具有Self-requirements协议.

以下示例将编译得很好,但会导致任意运行时效果发生:

// 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()
Run Code Online (Sandbox Code Playgroud)

您可以在此处查看IBM Swift Sandbox上的示例.在我的浏览器中,输出如下:

SuperMario.getFriend()
Throwing 32 fireballs!

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

这是Swift目前不是类型安全的证据吗?

编辑#1:

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

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

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

也许真正的定义不是那些.如果对语言有更多经验的人能够对这个话题有所了解,那就太棒了.

Alb*_*oda 6

是的,似乎存在矛盾.Self关键字在用作返回类型时,显然意味着"自我为自我的实例".例如,给定此协议

protocol ReturnsReceived {

    /// Returns other.
    func doReturn(other: Self) -> Self
}
Run Code Online (Sandbox Code Playgroud)

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

class Return: ReturnsReceived {

    func doReturn(other: Return) -> Self {
        return other    // Error
    }
}
Run Code Online (Sandbox Code Playgroud)

因为我们得到一个编译器错误("无法将类型'返回'的返回表达式转换为返回类型'Self'"),如果我们违反了doReturn()的合同并返回self而不是其他,则会消失.我们不能写

class Return: ReturnsReceived {

    func doReturn(other: Return) -> Return {    // Error
        return other
    }
}
Run Code Online (Sandbox Code Playgroud)

因为这只允许在最后一个类中,即使Swift支持协变返回类型.(以下实际编译.)

final class Return: ReturnsReceived {

    func doReturn(other: Return) -> Return {
        return other
    }
}
Run Code Online (Sandbox Code Playgroud)

另一方面,正如您所指出的,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
    }
}
Run Code Online (Sandbox Code Playgroud)

我错了,但我认为:

  • 如果Self作为返回类型实际上意味着"自我作为Self的实例",编译器不应该接受这种自我要求覆盖,因为它可以返回非自身的实例; 除此以外,

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

也就是说,在这里任何关于Self的精确语义的选择都不一定会改变事物,你的代码说明了编译器很容易被愚弄的情况之一,它能做的最好的事情就是生成代码来推迟检查运行 - 时间.在这种情况下,应该委托给运行时的检查与转换有关,在我看来,你的例子揭示的一个有趣的方面是,在特定的地方,Swift似乎不会委托任何东西,因此不可避免的崩溃更具戏剧性比它应该的.

Swift能够在运行时检查强制转换.我们考虑以下代码.

let sm = SuperMario()
let ffm = sm as! FireFlowerMario
ffm.throwFireballs()
Run Code Online (Sandbox Code Playgroud)

在这里,我们创建一个SuperMario并将其向下转换为FireFlowerMario.这两个类并不是无关的,我们正在向编译器(作为!)保证我们知道我们正在做什么,因此编译器保持原样并编译第二行和第三行而没有任何障碍.但是,该程序在运行时失败,抱怨它

Could not cast value of type
'SomeModule.SuperMario' (0x...) to
'SomeModule.FireFlowerMario' (0x...).
Run Code Online (Sandbox Code Playgroud)

在第二行尝试演员时.这不是错误或令人惊讶的行为.例如,Java将完全相同:编译代码,并在运行时使用ClassCastException失败.重要的是应用程序在运行时可靠地崩溃.

你的代码是一种更复杂的方式来欺骗编译器,但它归结为同样的问题:有一个SuperMario而不是FireFlowerMario.不同的是,在你的情况下,我们没有得到一个温和的"无法投射"消息,但是,在一个真正的Xcode项目中,调用throwFireballs()时出现了一个突然而且非常糟糕的错误.

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

相反,就我目前所见,Swift在调用之前不会尝试任何强制转换(在编译的代码中没有调用强制转换例程),因此一个可怕的,未被捕获的错误是唯一可能的结果.相反,如果你的代码产生了一个运行时"无法投射"的错误信息,或者那种优雅的东西,我会完全满意这种语言的行为.