为什么在force unwrapped nil可选值类型上使用dynamicType有效?

时间:2016-04-24 13:56:39

标签: swift

我真的很困惑,发现下面的代码只是拒绝使用典型的“意外发现的nil,同时展开一个可选值”异常,因为你需要强行解包{{1} }。

bar

似乎struct Foo { var bar: Bar? } struct Bar {} var foo = Foo() debugPrint(foo.bar) // nil debugPrint(foo.bar!.dynamicType) // _dynamicType.Bar 以某种方式能够回退到定义的dynamicType类型 - 而不会崩溃。

请注意,只有将bar定义为类型(@dfri says),BarFoostruct(作为pointed out by @MartinR)& final class是可变的。

我最初认为它可能只是一个编译器优化,因为foo的类型在编译时是已知的,因此力展开可能已被优化掉了 - 但它也会在崩溃时崩溃bar被定义为Bar。此外,如果我将“优化级别”设置为final class,它仍然有效。

我倾向于认为这是一个奇怪的错误,但想要一些确认。

这是-Onone的错误或功能,还是我在这里遗漏了什么?

(使用Xcode 7.3 w / Swift 2.2)

Swift 4.0.3更新

在Swift 4.0.3中,这仍然是可重现的(有一个更简单的例子):

dynamicType

我们在这里使用var foo: String? print(type(of: foo!)) // String 的后继者dynamicType来获取动态类型;和前面的例子一样,它不会崩溃。

2 个答案:

答案 0 :(得分:1)

这是indeed a bug,已修复by this pull request,这应该会成为Swift 4.2的发布版本,一切顺利。

如果有人对看似奇怪的要求再现它感兴趣,这里有一个(不是真的)简要介绍发生了什么...

标准库函数type(of:)的调用由类型检查器作为特殊情况解析;它们在AST中被一个特殊的“动态类型表达式”取代。我没有调查它的前任dynamicType是如何处理的,但我怀疑它做了类似的事情。

当为这样的表达式发出中间表示(SIL特定)时,the compiler checks to see如果生成的元类型为“thick”(对于类和协议类型的实例),如果是,则发出子表达式表达式(即传递的参数)并获取其动态类型。

但是,如果生成的元类型为“thin”(对于结构和枚举),编译器会在编译时知道元类型值。因此,只有具有副作用的子表达式才需要进行评估。这样的表达式是作为“被忽略的表达式”发出的。

问题在于发出忽略表达式的逻辑,这些表达式也是左值(一个可以分配给inout的表达式。)

Swift左值可以由多个组件组成(例如,访问属性,执行强制解包等)。有些组件是“物理的”,这意味着它们产生了一个可以使用的地址,而其他组件是“逻辑的”,这意味着它们包含一个getter和setter(就像计算变量一样)。

问题是物理组件被错误地认为是无副作用的;但是,力展开是一个物理组件,并且无副作用(键路径表达式也是非纯物理组件)。

因此,如果只有物理组件构成,则使用强制解包组件忽略表达式左值将错误地不会评估力展开。

让我们看一下目前崩溃的几个案例(在Swift 4.0.3中),并解释为什么这个bug是侧面步骤并且力量展开被正确评估了:

let foo: String? = nil
print(type(of: foo!)) // crash!

在这里,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!

这里,foo 左值,但编译器发现生成的元类型是“粗”,因此取决于foo!的值,所以左值加载,因此评估力展开。

让我们来看看这个有趣的案例:

class Foo {
  var bar: Bar?
}
struct Bar {}

var foo = Foo()
print(type(of: foo.bar!)) // crash!

崩溃了,但如果Foo被标记为final则不会崩溃。生成的元类型在任何一种情况下都是“瘦”,那么Foo final会产生什么差异?

好吧,当Foo不是final时,编译器不能只通过地址引用属性bar,因为它可能会被子类覆盖,这可能会重新实现它计算属性。因此,左值将包含逻辑组件(对bar的getter的调用),因此编译器将执行加载以确保评估此getter调用的潜在副作用(并且力量展开也将在负载中进行评估。

但是当Foofinal时,对bar的属性访问可以建模为物理组件,即可以通过地址引用。因此,编译器错误地认为因为所有左值组件都是物理组件,所以它可能会跳过评估它。

无论如何,这个问题现在已经解决了。希望有人发现上述漫游有用和/或有趣:)

答案 1 :(得分:0)

因为dynamicType对类型而不是值进行操作。在运行时......

foo.bar.dynamicType == Swift.Optional<Bar>

很自然地,当你解开可选项时,你得到Bar