Swift 5.7 使用 'if let a' 替换 'if let a = a'

2022-06-28 08:35:54 浏览数 (1)

介绍:

Swift 5.6 中比较常见的可选值解包绑定是使用 if let foo = foo { ... } 来对可选值解包,从而隐藏真正的可选值。这种模式要求开发者重复引用变量标识符 2 次,这样写的缺点在于解包时表达时会显得冗余,尤其是当变量名很长时。所以我们希望为可选值解包引入一种更简短的语法,类似以下语法:

代码语言:Swift复制
let foo: Foo? = ...

if let foo {
	// 'foo' is of type `Foo`
}

目的

减少重复定义,尤其是对特别长的变量名,让代码可读性更强。

举个例子,下面例子中对可选值someLengthyVariableNameanotherImportantVariable的解包读写性算是比较差:

代码语言:Swift复制
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,这点与现有的可选绑定一样。比如下面例子:

代码语言:Swift复制
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性能预测的路线图 中讨论了新的 refinout 引用标识,用于创建现有变量,而不需要复制变量(通过强制独占内存访问)。为了与 let / var 保持一致,支持这些新引用操作符也会变得有意义。支持之前:

代码语言:Swift复制
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 不可选变量。

另外一种方式是使用新操作符 refinout (这个概念上节提到过)。这些新操作符会让编译器独占访问变量内存,也就是直接对底层地址存储的访问,因此这种方式不需要在内部作用域上使用唯一标志符名称,也不需要进行地址复制,它将直接允许我们解包对象的成员。例如:

代码语言:Swift复制
// `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

0 人点赞