This is 确实是一个错误 https://bugs.swift.org/browse/SR-1327,已修复通过这个拉取请求 https://github.com/apple/swift/pull/14410,这应该会进入 Swift 4.2 的版本,一切进展顺利。
如果有人对重现它的看似奇怪的要求感兴趣,这里有一个(不是真正的)简要概述正在发生的事情......
调用标准库函数type(of:)
由类型检查器作为特殊情况解决;它们在 AST 中被特殊的“动态类型表达式”替换。我没有研究它的前身如何dynamicType
已处理,但我怀疑它做了类似的事情。
当为这样的表达式发出中间表示(具体来说是 SIL)时,编译器检查以查看 https://github.com/apple/swift/blob/34e77c0f258a989fd800d4b471d136fc11151558/lib/SILGen/SILGenExpr.cpp#L2846如果生成的元类型是“厚”(对于类和协议类型实例),如果是,则发出子表达式(即传递的参数)并获取其动态类型。
However,如果生成的元类型是“薄”的(对于结构和枚举),则编译器在编译时就知道元类型值。因此,只有当子表达式有副作用时才需要对其求值。这样的表达式被发出为“被忽略的表达式”。
问题在于发出被忽略的表达式的逻辑,这些表达式也是左值(可以分配给并传递为左值的表达式)inout
).
Swift 左值可以由多个组件组成(例如,访问属性、执行强制解包等)。一些组件是“物理的”,这意味着它们产生一个可以使用的地址,而其他组件是“逻辑的”,这意味着它们由 getter 和 setter 组成(就像计算变量一样)。
问题是physical组件被错误地认为是无副作用的;然而,力展开是一个物理组成部分,并且是not无副作用(键路径表达式也是非纯物理组件)。
因此,如果带有强制展开组件的忽略表达式左值仅由物理组件组成,则它们将错误地不评估强制展开。
让我们看一下当前崩溃的几个案例(在 Swift 4.0.3 中),并解释为什么该错误被回避并且强制展开被正确评估:
let foo: String? = nil
print(type(of: foo!)) // crash!
Here, foo
不是左值(正如它所声明的那样let
),所以我们只需获取它的值并强制展开。
class C {} // also crashes if 'C' is 'final', the metatype is still "thick"
var foo: C? = nil
let x = type(of: foo!) // crash!
Here, foo
is左值,但编译器认为生成的元类型是“厚”的,因此取决于foo!
,因此加载左值,并因此评估强制展开。
我们也来看看这个有趣的案例:
class Foo {
var bar: Bar?
}
struct Bar {}
var foo = Foo()
print(type(of: foo.bar!)) // crash!
它会崩溃,但不会崩溃Foo
被标记为final
。无论哪种方式,生成的元类型都是“薄”的,那么有什么区别呢?Foo
being final
make?
那么,当Foo
是非最终的,编译器不能只引用该属性bar
通过地址,因为它可能被子类覆盖,子类很可能将其重新实现为计算属性。因此,左值将包含一个logical组件(调用bar
的 getter),因此编译器将执行加载以确保评估此 getter 调用的潜在副作用(并且强制解包也将在加载中评估)。
然而当Foo
is final
, 属性访问bar
可以建模为物理组件,即可以通过地址引用。因此,编译器错误地认为,由于所有左值的组件都是物理的,因此它可能会跳过对其求值。
无论如何,这个问题现在已经解决了。希望有人觉得上面的闲聊有用和/或有趣:)