我真的很困惑,发现下面的代码只是拒绝崩溃与典型的" 意外发现的nil,同时解开一个可选值 "异常,你期望从力展开bar
.
struct Foo {
var bar: Bar?
}
struct Bar {}
var foo = Foo()
debugPrint(foo.bar) // nil
debugPrint(foo.bar!.dynamicType) // _dynamicType.Bar
Run Code Online (Sandbox Code Playgroud)
这似乎dynamicType
是在某种程度上能够回退到定义类型的bar
-没有崩溃.
请注意,这仅在Bar
定义为值类型(如@dfri所说)时出现,Foo
是a struct
或final class
(由@MartinR指出)foo
并且是可变的.
我最初认为它可能只是一个编译器优化,因为bar
在编译时已知类型,因此力展开可能已被优化掉 - 但它也Bar
被定义为a 时崩溃final class
.此外,如果我将"优化级别"设置为-Onone
,它仍然有效.
我倾向于认为这是一个奇怪的错误,但想要一些确认.
这是一个错误或功能dynamicType
,或者我只是在这里遗漏了一些东西?
(使用Xcode 7.3 w/Swift 2.2)
在Swift 4.0.3中,这仍然是可重现的(有一个更简单的例子):
var foo: String?
print(type(of: foo!)) // String
Run Code Online (Sandbox Code Playgroud)
在这里,我们使用dynamicType
的是继承者type(of:)
,以获得动态类型; 和前面的例子一样,它不会崩溃.
这确实是一个错误,已通过此拉取请求修复,这应该会进入 Swift 4.2 的版本,一切顺利。
\n\n如果有人\xe2\x80\x99s对重现它的看似奇怪的要求感兴趣,这里\xe2\x80\x99s对正在发生的事情进行了(不是真正的)简要概述......
\n\n对标准库函数的调用type(of:)
由类型检查器解析为特殊情况;它们\xe2\x80\x99在AST中被特殊的\xe2\x80\x9c动态类型表达式\xe2\x80\x9c替换。我没有调查它的前身dynamicType
是如何处理的,但我怀疑它做了类似的事情。
当为此类表达式发出中间表示(具体为 SIL)时,编译器会检查生成的元类型是否为 \xe2\x80\x9cthick\xe2\x80\x9d (对于类和协议类型实例),并且如果是,则发出子表达式(即传递的参数)并获取其动态类型。
\n\n但是,如果生成的元类型为 \xe2\x80\x9cthin\xe2\x80\x9d (对于结构和枚举),则编译器在编译时知道元类型值。因此,只有当子表达式有副作用时才需要对其求值。这样的表达式作为 \xe2\x80\x9cignored 表达式\xe2\x80\x9d 发出。
\n\n问题在于发出被忽略的表达式的逻辑,这些表达式也是左值(可以分配给并作为 传递的表达式inout
)。
Swift 左值可以由多个组件组成(例如,访问属性、执行强制解包等)。一些组件是 \xe2\x80\x9cphysical\xe2\x80\x9d,这意味着它们产生一个可以使用的地址,而其他组件是 \xe2\x80\x9clogic\xe2\x80\x9d,这意味着它们由 getter 组成和设置器(就像计算变量一样)。
\n\n问题在于物理组件被错误地认为是无副作用的。然而,强制展开是一个物理组件,并且并非没有副作用(关键路径表达式也是一个非纯物理组件)。
\n\n因此,如果带有强制展开组件的忽略表达式左值\xe2\x80\x99仅由物理组件组成,则它们将错误地不评估强制展开。
\n\n让\xe2\x80\x99s 看一下当前崩溃的几个案例(在 Swift 4.0.3 中),并解释为什么错误被回避并且强制解包被正确评估:
\n\nlet foo: String? = nil\nprint(type(of: foo!)) // crash!\n
Run Code Online (Sandbox Code Playgroud)\n\n这里,foo
不是一个左值(因为它声明了\xe2\x80\x99 let
),所以我们只是获取它的值并强制解开。
class C {} // also crashes if \'C\' is \'final\', the metatype is still "thick"\nvar foo: C? = nil\nlet x = type(of: foo!) // crash!\n
Run Code Online (Sandbox Code Playgroud)\n\n这里,foo
是一个左值,但编译器看到生成的元类型是 \xe2\x80\x9cthick\xe2\x80\x9d,因此取决于 的值foo!
,因此加载左值,并因此评估强制展开。
让\xe2\x80\x99s也看一下这个有趣的案例:
\n\nclass Foo {\n var bar: Bar?\n}\nstruct Bar {}\n\nvar foo = Foo()\nprint(type(of: foo.bar!)) // crash!\n
Run Code Online (Sandbox Code Playgroud)\n\nFoo
它会崩溃,但如果标记为则不会\xe2\x80\x99t final
。无论哪种方式,生成的元类型都是 \xe2\x80\x9cthin\xe2\x80\x9d ,那么存在有什么区别Foo
呢final
?
好吧,当Foo
是非最终的时,编译器不能只bar
通过地址引用属性,因为它可能会被子类覆盖,子类很可能将其重新实现为计算属性。因此,左值将包含一个逻辑组件(对 \xe2\x80\x99s getter 的调用bar
),因此编译器将执行加载以确保评估此 getter 调用的潜在副作用(并且还将评估强制展开在负载中)。
然而,当Foo
is 时final
,对属性的访问bar
可以建模为物理组件,即可以通过地址来引用。因此,编译器错误地认为,由于所有左值的组件都是物理的,因此它可能会跳过对其求值。
无论如何,这个问题现在已经解决了。希望有人觉得上面的闲聊有用和/或有趣:)
\n