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

问题描述 投票:10回答:2

我真的很困惑,发现下面的代码只是拒绝崩溃与典型的“意外发现nil同时解开一个可选值”异常,你期望从力展开bar

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

var foo = Foo()

debugPrint(foo.bar) // nil
debugPrint(foo.bar!.dynamicType) // _dynamicType.Bar

似乎dynamicType能以某种方式回退到定义类型的bar - 没有崩溃。

请注意,这只会在Bar定义为值类型(如@dfri says)时出现,Foostructfinal class(如pointed out by @MartinR)和foo是可变的。

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

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

这是dynamicType的错误或功能,还是我在这里遗漏了一些东西?

(使用Xcode 7.3 w / Swift 2.2)


Swift 4.0.3 update

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

var foo: String?
print(type(of: foo!)) // String

在这里,我们使用dynamicType的继任者type(of:)来获得动态类型;和前面的例子一样,它不会崩溃。

swift
2个回答
1
投票

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


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

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

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

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

问题在于发出忽略表达式的逻辑,这些表达式也是左值(一个可以分配给inout并作为let foo: String? = nil print(type(of: foo!)) // crash! 传递的表达式)。

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

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

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

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

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则不会崩溃。由此产生的元型都是“薄”,所以finalFoo有什么区别?

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

然而,当finalbar时,对foo.bar.dynamicType == Swift.Optional<Bar> 的属性访问可以被建模为物理组件,即它可以通过地址来引用。因此编译器错误地认为因为所有左值的组件都是物理的,所以它可能会跳过评估它。

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


0
投票

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

Bar

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

© www.soinside.com 2019 - 2024. All rights reserved.