介绍:
Swift 5.6 中比较常见的可选值解包绑定是使用 if let foo = foo { ... }
来对可选值解包,从而隐藏真正的可选值。这种模式要求开发者重复引用变量标识符 2 次,这样写的缺点在于解包时表达时会显得冗余,尤其是当变量名很长时。所以我们希望为可选值解包引入一种更简短的语法,类似以下语法:
let foo: Foo? = ...
if let foo {
// 'foo' is of type `Foo`
}
目的
减少重复定义,尤其是对特别长的变量名,让代码可读性更强。
举个例子,下面例子中对可选值someLengthyVariableName
和anotherImportantVariable
的解包读写性算是比较差:
let someLengthyVariableName: Foo? = ...
let anotherImportantVariable: Bar? = ...
if let someLengthyVariableName = someLengthyVariableName, let anotherImportantVariable = anotherImportantVariable {
...
}
解包的变量名太长了,虽然可以通过重命名变量名来它变得更短些,但是语义化就不那么明确,比如使用 a 和 b 代替:
代码语言:Swift复制if let a = someLengthyVariableName, let b = anotherImportantVariable {
...
}
这种方式描述可选变量解包之后的值就不够明确,在上下文重复调用 a 和 b 时,你并不是一直清楚 a 和 b 表示的准确含义。由于我们要隐藏可选值,那么解包之后的变量应该尽量还原本意,这种做法其实降低了语义化。语言设计准则中并不鼓励使用简短缩写的变量名称,而是应该考虑使用描述变量名称的语义化设计。这个问题将在 Swift 5.7 得到解决。
Swift5.7 中提议的解决方案:
如果我们删除解包中右边的表达式,让编译器来自动隐藏当前的变量,只保留左边的变量名,这时这些可选绑定表达会更加简洁,而且可读性更强。
代码语言:Swift复制let someLengthyVariableName: Foo? = ...
let anotherImportantVariable: Bar? = ...
if let someLengthyVariableName, let anotherImportantVariable {
...
}
这种方案既保持最清晰的语义(someLengthyVariableName),又减少了冗余的定义,因为对开发者来说,不需要写= someLengthyVariableName
, 只需要写一个核心的 someLengthyVariableName
, 这个扩展方案对可选绑定条件的现有语法来说,是不是特别自然?
设计细节
现在所有的条件控制都可以使用上述语法:
代码语言:Swift复制if let foo { ... }
if var foo { ... }
else if let foo { ... }
else if var foo { ... }
guard let foo else { ... }
guard var foo else { ... }
while let foo { ... }
while var foo { ... }
编译器会合成一个被隐藏变量的初始化表达式,例如:
代码语言:Swift复制if let foo { ... }
会在编译时被转为:
代码语言:Swift复制if let foo = foo { ... }
同时编译器也允许你显式声明类型,比如:
代码语言:Swift复制if let foo: Foo { ... }
会在编译时转为:
代码语言:Swift复制if let foo: Foo = foo { ... }
后面的模式既是一个计算表达式,也是新定义的非可选变量的标识符。此类语法的现有先例包括闭包捕获列表,其工作方式相同:
代码语言:Swift复制let foo: Foo
let closure = { [foo] in // `foo` is both an expression and the identifier
... // for a new variable defined within the closure
}
所以,这个语法上只能用合法的标志符,例如,下面这个例子就不合法:
代码语言:Swift复制if let foo.bar { ... } // ? unwrap condition requires a valid identifier
^ // fix-it: insert `<#identifier#> = `
如果支持访问对象的属性及成员,是不是更加强大?这点再下面的展望章节中也有提到。
新语法同样支持识别隐式 self
,这点与现有的可选绑定一样。比如下面例子:
struct UserView: View {
let name: String
let emailAddress: String?
var body: some View {
VStack {
Text(user.name)
// 和 `if let emailAddress = emailAddress { ... }` 等价
// 解包 `self.emailAddress`
if let emailAddress {
Text(emailAddress)
}
}
}
}
后续展望
Swift 5.7 已经实现了该提议,但是社区还是有不少的提议,比如支持可选转换,支持成员属性等。下面再看下这几点。
支持使用可选转换
未来可以扩展该语法,支持对可选值的类型转换,比如:
代码语言:Swift复制if let foo as? Bar { ... }
其实也就是等价于:
代码语言:Swift复制if let foo = foo as? Bar { ... }
这种在日常开发中很常见,比如在解包参数 Any?, AnyObject?,T? 等为某个确定类型.
支持 ref
、&
引用操作
提高Swift性能预测的路线图 中讨论了新的 ref
和 inout
引用标识,用于创建现有变量,而不需要复制变量(通过强制独占内存访问)。为了与 let
/ var
保持一致,支持这些新引用操作符也会变得有意义。支持之前:
if ref foo = foo {
// if `foo` is not nil, it is borrowed and made available as a non-optional, immutable variable
}
if inout foo = &foo {
// if `foo` is not nil, it is borrowed and made available as a non-optional, mutable variable
}
使用新语法支持之后, 我们可以这样写:
代码语言:Swift复制if ref foo {
// if `foo` is not nil, it is borrowed and made available as a non-optional, immutable variable
}
if inout &foo {
// if `foo` is not nil, it is borrowed and made available as a non-optional, mutable variable
}
支持解包对象的成员
当前提议并没有实现对其他对象里的成员进行解包简写支持。比如下面的语法会出错:
p
代码语言:Swift复制if let foo.bar { ... } // ?
其实有几个方式可以考虑用来支持上述这种语法访问。
第一种方式是在解包变量的内在作用域内,编译器自动合成标志符名称。比如,编译器会对 if let foo.bar
引入 一个新的名为foo
或者 fooBar
不可选变量。
另外一种方式是使用新操作符 ref
和 inout
(这个概念上节提到过)。这些新操作符会让编译器独占访问变量内存,也就是直接对底层地址存储的访问,因此这种方式不需要在内部作用域上使用唯一标志符名称,也不需要进行地址复制,它将直接允许我们解包对象的成员。例如:
// `mother.father.sister` is optional
if ref mother.father.sister {
// `mother.father.sister` is non-optional and immutable
}
if inout &mother.father.sister {
// `mother.father.sister` is non-optional and mutable
}
其他
社区还有几个其他的备选方案来实现if let
, 有兴趣可以看提议中的Alternatives considered
章节。
Proposal: 0345-if-let-shorthand.md