【问题标题】:Why does using dynamicType on a force unwrapped nil optional value type work?为什么在强制展开的 nil 可选值类型上使用 dynamicType 有效?
【发布时间】:2016-04-24 13:56:39
【问题描述】:

我很困惑地发现下面的代码根本拒绝崩溃,典型的“Unexpectedly found nil while unwrapping an Optional value”你期望强制解包bar的异常。

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

var foo = Foo()

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

似乎dynamicType以某种方式能够回退到bar的定义类型——而不会崩溃。

请注意,这仅在 Bar 定义为 value 类型(如 @dfri says)、Foostructfinal class(如 pointed out by @MartinR ) & foo 是可变的。

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

我倾向于认为这是一个奇怪的错误,但希望得到一些确认。

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

(使用带有 Swift 2.2 的 Xcode 7.3)


Swift 4.0.3 更新

这在 Swift 4.0.3 中仍然可以重现(用一个更小的例子):

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

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

【问题讨论】:

  • 请注意,它与class Foo 崩溃,但不是final class Foo ... :)
  • 您说的是在编译时知道dynamicType 的结果的情况。 ! 可能是一个错误。
  • 我想知道它是否与使模式匹配工作的同一件事有关 - 这可能听起来很愚蠢,但我的意思是,它能够像 sealed 一样使用提示 final 并工作倒退……对我来说,这闻起来像是一种无意的行为,但我知道吗:)
  • @Sulthan 我确实认为这可能只是编译器优化,因为bar 的类型在编译时是已知的,因此可以优化强制展开 - 但是当Bar 被定义为final class 时它会崩溃,所以我不太确定!
  • 似乎允许上述内容在没有运行时异常的情况下运行的一个关键是 bar任何类型的值 类型,而不仅仅是结构或枚举(并且foo 是一个可变的)。例如。 struct Foo { var bar: Int? }; var foo = Foo() 同样允许在强制解包(空)可选 foo.bar 上使用 dynamicType(而例如 bar 作为基础:s NSNumber 引用类型将产生崩溃)。

标签: swift


【解决方案1】:

这里是indeed a bug,已经修复了by this pull request,应该会在Swift 4.2 发布,一切顺利。


如果有人对重现它的看似奇怪的要求感兴趣,这里(不是真的)简要概述了正在发生的事情......

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

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

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

问题在于发出同样是左值的被忽略表达式的逻辑(可以分配给inout并作为inout传递的表达式)。

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

问题在于,物理组件被错误地假定为无副作用;然而,强制展开是一个物理组件,不是没有副作用的(键路径表达式也是一个非纯物理组件)。

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

让我们看一下当前崩溃的几个案例(在 Swift 4.0.3 中),并解释为什么该错误被回避并且正确评估了强制展开:

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 一个左值,但是编译器看到生成的元类型是“thick”,因此依赖于foo!的值,所以加载了左值,并且因此会评估展开力。

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

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

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

它会崩溃,但如果 Foo 被标记为 final,它不会崩溃。无论哪种方式,生成的元类型都是“瘦”的,那么 Foo 成为 final 有什么区别?

好吧,当Foo 不是最终的,编译器不能只通过地址引用属性bar,因为它可能被子类覆盖,这很可能将其重新实现为计算属性。因此,左值将包含一个 logical 组件(对bar 的 getter 的调用),因此编译器将执行加载以确保评估此 getter 调用的潜在副作用(以及force unwrap 也将在负载中进行评估)。

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

不管怎样,这个问题现在已经解决了。希望有人发现上述闲聊有用和/或有趣:)

【讨论】:

    【解决方案2】:

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

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

    所以很自然,当您打开可选项时,您会得到Bar

    【讨论】:

    • dynamicType 绝对是对值进行操作,否则它不是动态的!你是对的,foo.bar 的动态类型是Swift.Optional&lt;Bar&gt; - 但我试图在这里获得foo.bar! 的动态类型。由于foo.barSwift.Optional&lt;Bar&gt;.None,即使bar 的类型信息在强制展开后以某种方式推断(它似乎是),也不能回答如何它能够避免首先强制解开 .None 可选时应该发生的致命错误。
    猜你喜欢
    • 2019-01-29
    • 1970-01-01
    • 2018-11-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多