隐式解包可选类型的重新实现
今年早些时候,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!
是一种更普遍问题的特殊情况:将 !
用作类型的一部分。
有三个地方允许将 !
用作类型的一部分
- 属性声明
- 函数声明中的参数
- 函数声明中的返回值
在其他位置,!
应该被标记为错误,并且 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 let
和 guard 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 let
和 guard let
使用显式解包。当您确定安全时,请通过 !
使用显式强制解包。
有问题或意见?
如果您对此帖子有任何问题或意见,请随时在 Swift 论坛上的这个相关主题中跟进。