隐式解包可选类型的重新实现

今年早些时候,Swift 编译器中引入了一个新的隐式解包可选类型(IUO)的实现,并且可以在最近的 Swift 快照版本 中试用。 这完成了 SE-0054 - 废除隐式解包可选类型 的实现。 这是对语言的一个重要更改,它消除了类型检查中的一些不一致性,并阐明了如何处理这些值的规则,使其具有一致性且易于推理。 有关更多信息,请参阅该提案的 动机部分

您将看到的主要变化是,当引用声明为具有底层类型 T 的隐式解包可选类型的值时,诊断信息现在将打印 T? 而不是 T!。 您可能还会遇到源代码兼容性问题,需要您修改代码才能成功编译。

隐式解包是声明的一部分

隐式解包可选类型 是指如果表达式需要编译,则会自动解包的可选类型。 要声明一个隐式解包的可选类型,请在类型名称后放置一个 ! 而不是 ?

许多人对隐式解包可选类型的心理模型是,它们是一种类型,与常规可选类型不同。 在 Swift 3 中,它们的工作方式正是如此:像 var a: Int? 这样的声明将导致 a 具有类型 Optional<Int>,而像 var b: String! 这样的声明将导致 b 具有类型 ImplicitlyUnwrappedOptional<String>

IUO 的新心理模型是,您将 ! 视为 ? 的同义词,但添加了一个标志在声明中,让编译器知道声明的值可以被隐式解包。

换句话说,您可以将 String! 理解为 “这个值的类型是 Optional<String>,并且还携带信息表明它可以在需要时被隐式解包”。

这种心理模型与新的实现相匹配。 在您拥有 T! 的任何地方,编译器现在都将其视为具有类型 T?,并在其声明的内部表示中添加一个标志,以告知类型检查器可以在必要时隐式解包该值。

此更改最明显的结果是,您现在将看到关于使用 T! 声明的值的诊断信息中谈论 T? 而不是 T!。 在诊断信息中看到 T? 而不是 T! 需要一些时间来适应,但接受这种新的心理模型应该对您有所帮助。

源代码兼容性

大多数项目应该可以构建,而不会遇到兼容性问题。 但是,这些实现更改可能会导致行为发生变化,这些变化与 SE-0054 一致,但与以前的编译器版本不一致。

强制转换为 T!

SE-0054 禁止了 as T! 形式的强制转换。

在 Swift 4.1 中,对于这些强制转换存在弃用警告。 在许多情况下,将 as T! 替换为 as T?,或者 просто 删除强制转换,都可以成功编译。

在许多情况下,现有代码使用这两种更改之一都无法编译,因此在新实现中对此进行了特殊处理。 具体来说,如果您编写 x as T!,编译器将首先尝试将此类型检查为 x as T?。 只有当该操作失败时,编译器才会尝试将其类型检查为 (x as T?)!,强制转换为可选类型。

但是,这种形式的强制转换仍然被认为是已弃用的,并且这种特殊处理可能会在 Swift 的未来版本中删除。

在类型而不是声明上使用 !

强制转换为 T! 是一种更普遍问题的特殊情况:将 ! 用作类型的一部分。

有三个地方允许将 ! 用作类型的一部分

  1. 属性声明
  2. 函数声明中的参数
  3. 函数声明中的返回值

在其他位置,! 应该被标记为错误,并且 Swift 4.1 之前的版本试图这样做,但遗漏了一些情况

let fn: (Int!) -> Int! = ...   // error: not a function declaration!

Swift 4.1 在这些情况下发出弃用警告,但继续遵守隐式解包行为。 最近快照中的新实现将 ! 视为 ?,并发出诊断信息告诉您正在发生的事情以及在这些位置使用 ! 已被弃用。

在声明为隐式解包可选类型的值上调用 map

以前,像这样的代码

class C {}
let values: [Any]! = [C()]
let transformed = values.map { $0 as! C }

会导致强制解包 values,然后在数组上调用 map(_:)。 即使您在 ImplicitlyUnwrappedOptional 的扩展中定义了成员 map(_:),情况也是如此,因为成员查找 ImplicitlyUnwrappedOptional 的方式与预期不符。

在新的实现中,由于 !? 的同义词,编译器会尝试在此处调用 Optional<T> 上的 map(_:)

let transformed = values.map { $0 as! C } // calls Optional.map; $0 has type [Any]

并产生:warning: cast from '[Any]' to unrelated type 'C' always fails

因为这在技术上通过了类型检查,所以我们不会尝试强制解包 values

您可以通过使用可选链来生成可选数组来解决此问题

let transformed = values?.map { $0 as! C } // transformed has type Optional<[C]>

或者通过强制解包 values 来生成数组

let transformed = values!.map { $0 as! C } // transformed has type [C]

请注意,在许多情况下,您不会看到行为上的变化

let values: [Int]! = [1]
let transformed = values.map { $0 + 1 }

这仍然像以前一样工作,因为如果您在 Optional 上调用 map(_:),则无法成功进行类型检查表达式。 相反,我们最终会强制解包 values 并在结果数组上调用 map(_:)

您无法推断不是类型的类型

因为隐式解包可选类型不再是与可选类型不同的类型,所以它们不能被推断为类型或任何类型的一部分。

在下面的示例中,尽管赋值的右侧包含一个声明为隐式解包的值,但左侧的推断类型仅指示该值(或返回值)是可选的。

var x: Int!
let y = x   // y has type Int?

func forcedResult() -> Int! { ... }
let getValue = forcedResult    // getValue has type () -> Int?

func id<T>(_ value: T) -> T { return value }
let z = id(x)   // z has type Int?

func apply<T>(_ fn: () -> T) -> T { return fn() }
let w: Int = apply(forcedResult)    // fails, because apply() returns unforced Int?

您可能还会注意到此行为变化的一些特定实例包括 AnyObject 查找、try?switch

AnyObject 查找

请注意,AnyObject 查找的结果被视为隐式解包的可选类型。 如果您查找的属性本身也被声明为隐式解包,则表达式现在具有两个级别的隐式解包(property 被声明为 UILabel!

func getLabel(object: AnyObject) -> UILabel {
  return object.property // forces both optionals, resulting in a UILabel
}

if letguard let 仅解包一层可选性。

对于以下示例,以前版本的 Swift 在为 if let 解包一层可选类型后,推断 label 的类型为 UILabel!。 在快照构建中,Swift 将推断其为 UILabel?

// label is inferred to be UILabel?
if let label = object.property {
   // Error due to passing a UILabel? where a UILabel is expected
  functionTakingLabel(label)
}

这可以通过使用显式类型来修复

// Implicitly unwrap object.property due to explicit type.
if let label: UILabel = object.property {
  functionTakingLabel(label) // okay
}

try?

同样,try? 添加了一层可选性,因此当将 try? 与返回隐式解包值的函数组合时,您可能会发现现在需要修改代码以显式解包第二层可选性。

func test() throws -> Int! { ... }

if let x = try? test() {
  let y: Int = x    // error: x is an Int?
}

if let x: Int = try? test() { // explicitly typed as Int
  let y: Int = x    // okay, x is an Int
}

if let x = try? test(), let y = x { // okay, x is Int?, y is Int
 ...
}

switch

Swift 4.1 接受了以下代码,因为它将 output 视为隐式解包

func switchExample(input: String!) -> String {
  switch input {
  case "okay":
    return "fine"
  case let output:
    return output  // implicitly unwrap the optional, producing a String
  }
}

请注意,如果以这种方式编写,则无法成功编译

func switchExample(input: String!) -> String {
  let output = input  // output is inferred to be String?
  switch input {
  case "okay":
    return "fine"
  default:
    return output  // error: value of optional type 'String?' not unwrapped;
                   // did you mean to use '!' or '?'?
  }
}

新的实现在第一个示例中推断 output 的类型为 String?,这不是隐式解包的。

使其再次编译的一种方法是强制解包该值

  case let output:
    return output!

另一种解决方法是显式地为非 nil 和 nil 进行模式匹配

func switchExample(input: String!) -> String {
  switch input {
  case "okay":
    return "fine"
  case let output?: // non-nil case
    return output   // okay; output is a String
  case nil:
    return "<empty>"
  }
}

使用可选类型与隐式解包可选类型重载 In-Out 参数

Swift 4.1 为代码尝试重载函数的情况引入了弃用警告,其中区别在于 in-out 参数是普通可选类型还是隐式解包可选类型。

  func someKindOfOptional(_: inout Int?) { ... }

  // Warning in Swift 4.1.  Error in new implementation.
  func someKindOfOptional(_: inout Int!) { ... }

Swift 4.1 还添加了将声明为隐式解包的值作为 in-out 参数传递给期望普通可选类型的函数,反之亦然的功能。这使得删除上面的第二个重载成为可能(假设实现是相同的)。

  func someKindOfOptional(_: inout Int?) { ... }

  var i: Int! = 1
  someKindOfOptional(&i)   // okay! i has type Optional<Int>

由于隐式解包可选类型的新实现,考虑到 Int! 的类型是 Int? 的同义词,因此按可选性重载不再有意义。因此,像上面这样的重载现在会导致错误,并且必须删除第二个重载(使用 Int! 声明)。

ImplicitlyUnwrappedOptional 的扩展

ImplicitlyUnwrappedOptional<T> 现在是 Optional<T> 的不可用类型别名,并且尝试在该类型上创建扩展的代码将无法编译。

// 1:11: error: 'ImplicitlyUnwrappedOptional' has been renamed to 'Optional'
extension ImplicitlyUnwrappedOptional {

桥接 Nil

当桥接 nil 值时,nil 将被桥接到 NSNull,而不是遇到运行时故障。

import Foundation

class C: NSObject {}

let iuoElement: C! = nil
let array: [Any] = [iuoElement as Any]
let ns = array as NSArray
let element = ns[0] // Swift 4.1: Fatal error: Attempt to bridge
                    // an implicitly unwrapped optional containing nil

if let value = element as? NSNull, value == NSNull() {
  print("pass")     // We reach this statement with the new implementation
} else {
  print("fail")
}

结论

隐式解包可选类型已被重新实现,使得它们不再是与 Optional<T> 不同的类型。因此,类型检查更加一致,并且编译器中的特殊情况更少。删除这些特殊情况应减少处理这些声明时的错误。

您可能会因为与导入的 Objective-C API 交互而接触到隐式解包。您可能会偶尔发现使用隐式解包来声明 @IBOutlet 属性,或者在其他您知道在完全初始化之前不会访问值的地方很方便。但是,通常最好避免隐式解包,而应通过 if letguard let 使用显式解包。当您确定安全时,请通过 ! 使用显式强制解包。

有问题或意见?

如果您对此帖子有任何问题或意见,请随时在 Swift 论坛上的这个相关主题中跟进。