- 提案: SE-0230
- 作者: BJ Homer
- 审查管理员: John McCall
- 状态: 已实现 (Swift 5)
- 实现: apple/swift#16942
- 审查: (论坛帖子) (验收)
介绍
Swift 中的 try?
语句目前很容易引入嵌套可选(译者注:嵌套可选即类似 var foo: String??
可选的可选类型)。而用户难以推断嵌套可选的产生原因,所以 Swift 尝试避免在一些常见情况下产生嵌套可选。
该文档提议给予 try?
与其它常见的 Swift 功能中相同的可选摊平行为(译者注:可选摊平即将可选类型的值去掉可选的一层),来避免常见的嵌套可选。
Swift-evolution 帖子: 使 try? 与可选链摊平协同工作
动机
目前,使用 try?
非常容易产生嵌套的 Optional
类型。虽然构建嵌套可选是被允许的,但这通常并非是开发者所希望的。
Swift 拥有许多机制来避免意外创建嵌套可选。比如:
// 注意 as? 是如何可以无视被转换的值是否是可选类型的,其总是产生了相同的类型
let x = nonOptionalValue() as? MyType // x 是 'MyType?' 类型
let y = optionalValue() as? MyType // y 是 'MyType?' 类型
// 注意可选链可以无视调用者是否产生可选值,其总是产生了相同的类型
let a = optThing?.pizza() // a 是 'Pizza?' 类型
let b = optThing?.optionalPizza() // b 是 'Pizza?' 类型
但是 try?
的行为不同:
let q = try? harbor.boat() // q 是 'Boat?' 类型
let r = try? harbor.optionalBoat() // r 是 'Boat??' 类型
上述例子是特意举例的,但其实在生产环境的代码中,也很容易造成嵌套可选。举个例子:
// 由于 'foo' 的可选链,'foo?.makeBar()' 的结果是 'Bar?'。
// 'try?' 增加了额外一层可选。所以 'x' 类型是 'Bar??'
let x = try? foo?.makeBar()
// JSONSerialization.jsonObject(with:) 返回 'Any'。
// 我们使用 'as?' 来确认结果是否是期望的类型,但 'try?' 增加了额外一层可选,导致结果 'dict' 现在是 '[String: Any]??' 类型。
let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
虽然使用 try?
很容易造成嵌套可选,但对现存代码的一个调查显示,这并非是理想的结果。使用 try?
和嵌套可选的代码几乎总是伴随着以下几种模式之一:
// 模式 1:双层 if-let 或 guard-let
if let optionalX = try? self.optionalThing(),
let x = optionalX {
// 在这里使用 'x'
}
// 模式 2:引入括号并使用 'as?' 摊平
if let x = (try? somethingAsAny()) as? JournalEntry {
// 在这里使用 'x'
}
// 模式 3:模式匹配
if case let x?? = try? optionalThing() {
// 在这里使用 'x'
}
这些变通方法使得语言更加难以学习和使用,且没有任何好处或者回报。
使用 try?
的代码通常不关心错误和空结果的区别,这就是为什么所有这些模式只关注取出值而忽略错误。如果开发者关心错误,那么应该使用 do/try/catch
取而代之。
提议解决方案
在 Swift 5 中,try? someExpr()
将和 foo?.someExpr()
行为一致:
- 如果
someExpr()
返回非可选值,try? someExpr()
将会被包在可选中。 - 如果
someExpr()
返回Optional
,try? someExpr()
将不会再额外附加可选。
这将导致以下的 try?
表达式类型改变:
// Swift 4: 'Int??'
// Swift 5: 'Int?'
let result = try? database?.countOfRows(matching: predicate)
// Swift 4: 'String??'
// Swift 5: 'String?'
let myString = try? String(data: someData, encoding: .utf8)
// Swift 4: '[String: Any]??'
// Swift 5: '[String: Any]?'
let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
当子表达式返回非可选类型时,整体类型将不会发生改变。
// Swift 4: 'String?'
// Swift 5: 'String?'
let fileContents = try? String(contentsOf: someURL)
如果子表达式已经是嵌套可选,结果将和此嵌套层级一致:
func doubleOptionalInt() throws -> Int?? {
return 3
}
// Swift 4: 'Int???'
// Swift 5: 'Int??'
let x = try? doubleOptionalInt()
关于
try?
和as?
的额外说明虽然
as?
常有摊平可选类型的作用(如上在「动机」部分的举例),但这与提议的try?
并非是完全相同的行为。由于as?
采用明确的类型,所以它实际上可以摊平多个层级的嵌套可选。无论foo
上有多少层可选,foo as? T
将总是返回Optional<T>
。取决于指定的类型,as?
可以潜在地增加或减少可选的层级。(as?
同样可以在子类型与父类型之间转换,这与正在审议的行为无关。)在实践中,
as?
与嵌套可选最常使用在将T??
降级到T?
,这与使用可选链以及提议的try?
类似。但as?
是比提议的try?
功能更加丰富有力的结构。
细节设计
在 Swift 4 中,try?
表达式的类型被定义为 Optional<T>
,T
表示位于 try?
之后的表达式类型。
在 Swift 5 中,try?
表达式的类型将变为某种 U
,U
是 Optional<_>
类型且子表达式类型 T
能够强制转换为 U
。类型约束系统将自动选择满足约束的最小嵌套可选层级,这将导致本提案中描述的行为。
泛型
关于与泛型互通性的一些问题被提出,如下例:
func test<T>(fn: () throws -> T) -> T? {
// 如果 T 是可选类型,该行是否会改变行为?
if let result = try? fn() {
print("We got a result!")
return result
}
else {
print("There was an error")
return nil
}
}
// T 在这里被推断为 'Int'
let value = test({ return 15 })
// T 在这里被推断为 'Int?'
let value2 = test({ return 15 as Int? })
答案是这与 T
在运行时是否可选无关。在编译时刻,result
拥有清晰的类型:T
。这在 Swift 4 和 Swift 5 的模式中都是正确的,因为 T
在编译时刻无法得知是否是 Optional
类型,try?
表达式额外增加了一层 Optional
,之后又通过 if let
解包。
使用 try?
的泛型代码仍可继续使用,而无需关心泛型是否可能在运行时变成可选。这种情况的行为并没有发生改变。
源代码兼容
如果表达式自身没有明确摊平可选,这对于操作可选子表达式的 try?
表达式来说是一个打破源的改变。尽管目前出现的情况很少,但详见下面的分析。当编译器运行在 Swift 4 模式下,我们可以提供向后兼容的行为,且为大多数常见情况提供迁移的可能。
关于 Swift 5 中包含打破源变化的讨论很激烈,但我相信最终能通过。以下是 swift-evolution README 中列出的部分标准:
1. 当前的语法或 API 必须主动展示可能为用户带来的问题
嵌套可选是一个复杂的概念。其在语言中存在是有价值的,但对于其使用应当是目的明确的。由于目前 try?
和可选链或 as?
转换之间的交叉使用,初学者很容易不明所以地造成嵌套可选。
实际检测错误 try?
要比使用 try
要更加困难。对比:
// 使用 'try?'
if let result = try? foo?.bar() {
// 额外对 'result' 做些什么,尽管在 if let 内其仍可能为 nil
}
else {
//出现错误,但不清楚是什么
}
// 使用 'try/catch'
do {
let result = try foo?.bar()
// 额外对 'result' 做些什么,由于可选链其仍可能为 nil
}
catch {
// 处理错误
}
使用 try?
的变体将变得非常凌乱(比如关于 result
是什么类型?),而且没有显而易见的优点。在不关心 else 语句且只希望处理存在的值时,try?
是更好的选择,但提议的改变将更好的作用于这种情况。
由于与 as?
转换的交叉使用,因此当前的语法仍比较痛苦,如下:
if let x = try? foo() as? String {
// `String` 是这里想要得到的类型,
// 但意外的是 `x` 是 `Optional<String>` 类型
}
2. 新语法 / API 必须更加清晰且不与现存的 Swift 语法冲突
提出的变更更好地解决了以上所有的问题。此次变更同样明确了 try?
的角色,即在可能的情况下访问值,而非 try/catch
类似的错误处理机制。
3. 已存在的代码必须支持合理地自动迁移
如下分析,大多数源代码将无需迁移;已经遇到产生嵌套可选的开发者,可能已经使用了兼容模式。该提案简单地提供了一种简化的方式。
自动迁移已为双 if/guard let
和 case let value??:
这些上述已提到的模式实现。
Swift 源兼容性套件分析
Swift 源兼容性套件建议不要为大多数用户制造巨大的改变。我手动检查了 try?
在兼容性套件中的使用案例。以下是结果:
-
兼容性套件中总共有 613 个
try?
的用例。其中绝大多数使用在非可选子表达式中,不受该提议的影响。 -
try? ... as?
的用例有 4 个。它们都将try?
包裹在括号中以使用as?
的摊平行为,且这将是兼容的。它们看来像这样:(try? JSONSerialization.jsonObject(with: $0)) as? NSDictionary
-
3 个项目中共有 12 个
try? foo?.bar()
的用例。其中 10 个为_ = try? foo?.bar()
,所以结果的类型不重要。其中 2 个拥有Void
类型的子表达式,且没有赋值结果到任何变量。 -
有 6 个
try? somethingReturningOptional()
的用例。它们全都使用flatMap { $0 }
手动摊平,且因此兼容,因为该表达式的返回类型在任一行为下都是相同的。(try? get(key)).flatMap { $0 }
-
据我所知,整个套件中没有双可选被实际用来区分错误和 nil 转值的情况。
-
据我所知,兼容套件中没有发现任何源代码不兼容的情况。
ABI 稳定性的影响
对 ABI 无影响。
API 适应性的影响
try?
表达式永远不会暴露在函数边界,因此 API 适应性理应不受影响。
考虑过的替代方案
改变 try? 的绑定优先权
对于类似 let x = try? getObject() as? Foo
的表达式,可以通过转换表达式为 (try? getObject) as? Foo
,来消除嵌套可选。显式添加括号已经是双可选问题的常用变通方法。
但此举不能解决 try?
与可选链(例如:try? foo?.bar?.baz()
)的情况,以及不能解决从函数直接返回的一个可选结果(例如:try? cachedValue(for: key)
)。
改变 try?
的绑定优先权可能造成比该提案更加深远的源变化。
什么也不做
在当前的模式下写出正确的代码是可以的。我们并非提出完全从语言中移除嵌套可选,我们只是希望用户弄清楚它们。
这是一个可行的解决方案,但作为语法糖的一部分,为简化日常使用却拥有如此复杂的结构是很奇怪的。