介绍
SE-0360, Swift 5.7 已实现
自 SE-0244 引入以来,不透明的结果类型已成为类型级抽象的强大工具,允许库作者隐藏其API的实现细节。
根据 SE-0244 中描述的规则,返回不透明结果类型的函数必须从每个返回语句中返回与T
类型相同的值,并且T
必须满足不透明类型上所述的所有约束。
当前模型和实现限制了不透明结果类型作为抽象机制的有用性,因为它阻止了框架引入新类型并将其用作现有API 的基础类型。为了弥补这一可用性差距,本篇提议建议在可用条件下放宽对返回的同类型限制。
提议的动机
为了说明在不透明结果类型和可用性条件之间的交互问题,我们列举个框架例子,该框架下定义Shape
协议,并且Square
类型已经遵循该Shape
协议,如下:
protocol Shape {
func draw(to: Surface)
}
struct Square : Shape {
...
}
在该库的新版本中,库作者决定引入一个新的shape
- Rectangle
, 但是新类型被限制了可用性,在新的版本需要通过@available
来区分,例如:
@available(macOS 100, *)
struct Rectangle : Shape {
...
}
因为Rectangle
是Square
的变体,那么Square
是可以转化为Rectangle
。但是这种转化还是需要通过@available
限制可用性,原因是因为Rectangle
类型已经是受限制可用的。
@available(macOS 100, *)
extension Square {
func asRectangle() -> some Shape {
return Rectangle(...)
}
}
新方法asRectangle()
必须在可用性上下文中声明才能返回Rectangle
, 这限制了它的可用性,因为asRectangle()
的所有用途都封装在#avaliable
代码块中。
如果框架的老版本就存在asRectangle()
,不声明if #available
条件,就无法使用新类型:
struct Square {
func asRectangle() -> some Shape {
if #available(macOS 100, *) {
return Rectangle(...)
}
return self
}
}
但上述这样声明是不允许的,因为asRectangle()
函数主体中的所有返回语句都必须返回相同的具体类型。所以上述代码会报错:
error: function declares an opaque return type 'some Shape', but the return statements in its body do not have matching underlying types
func asRectangle() -> some Shape {
^ ~~~~~~~~~~
note: return statement has underlying type 'Rectangle'
return Rectangle()
^
note: return statement has underlying type 'Square'
return Square()
^
对于库作者来说,这是一个死胡同,尽管SE-0244指出,在未来版本的库/框架中可能更改基础结果类型,但这个假设是基于该类型已经存在,因此可以在所有返回语句中使用。恰巧在实际情况下,新类型往往是后续版本中新增,所以所绕进了一个死胡同。
提议解决方案
为了弥补上述可用性不足,本篇提议:放宽带有if #available
函数的同类型返回限制,如果if #available
条件一定会被执行,那么它可以返回与函数其余部分返回类型不同的类型。不要求必须返回同一类型。
这个提议给函数带来 2 点改变:
- 多个
if #available
可以根据它的动态性返回不同类型 - 可以安全返回一个确定类型,不受可用性限制,即使不符合任何可用性条件
由于函数中的返回类型在函数未运行时就要确认,即在声明函数时就要确认函数的返回类型,但如果把可用性条件判断(if #available
)和其他条件判断(比如,guard, if, switch)混合在一起,函数就无法确定返回类型, 并且需要在if #available
中返回与函数其余部分相同的类型。(意思是:在if #available
内部包含的所有if
, guard
, switch
的每个return
分支上,都需要返回相同类型)下面这个例子满足这些规则:
func test() -> some Shape {
if #available(macOS 100, *) { ✅
return Rectangle()
}
return self
}
详细设计
无条件可用子句称为
if #available
子句。
if #available
语句是满足下列条件的if
或者else if
语句:
- 该子句是其所在函数顶层
if
语句的一部分 - 在
if
条件语句之前,其所在函数还没有出现return
语句 - 子句的条件是一个
#available
条件 - 该子句要么是初始的
if
子句,要么是紧随无条件可用性子句之后的其他if
子句 - 该子句至少包含一个
return
语句 - 通过子句控制的所有结束路径都以
return
或者throw
结束
if #available
子句以外的所有返回语句必须返回彼此相同的类型,并且该类型必须与其所在函数一样可用。
所有在给定的if #available
子句内的return
语句必须每次返回相同的类型,这种类型必须与子句的#available
条件一样可用。并且这个类型不必与子句之外的其他任何返回类型相同。
在所在函数中,必须至少有一种return
语句。如果在无条件可用性子句之外,没有return
语句,那么在if #available
子句内部,至少存在一种返回类型与函数的返回返回相同。
函数的返回类型一般是:
- 第一个无条件动态满足的
if #available
子句的return
语句返回类型; - 所有的
if #available
子句之外的return
语句返回类型; - 第一个
if #available
子句的return
语句的返回类型,这个类型与函数返回类型一样
第一个示例是正确的,因为第一个if #available
和第二个if #available
都以return
结束,代码如下:
func test() -> some Shape {
if #available(macOS 100, *) { ✅
return Rectangle()
} else if #available(macOS 99, *) { ✅
return Square()
}
return self
}
但是如果把上述条件修改下:
代码语言:Swift复制func test() -> some Shape {
if cond {
...
} else if #available(macOS 100, *) { ❌
return Rectangle()
}
return self
}
编译器此时会报错,因为if #available
和动态条件相关联。
如果if #available
在可以return
的动态条件之后,这种情况也是不允许的。比如在函数开始时,提前 guard
并返回对象,那么这属于一个动态条件, 这种情况也会报错:
func test() -> some Shape {
guard let x = <opt-value> else {
return ...
}
if #available(macOS 100, *) { ❌
return Rectangle()
}
return self
}
同样,如果if #available
出现在某个循环内,也会报错:
func test() -> some Shape {
for ... {
if #available(macOS 100, *) { ❌
return Rectangle()
}
}
return self
}
下面例子中的 test()
方法符合上述规则,会通过编译器编译。该方法中if
条件在它的分支内都有结果,且返回结果类型相同。if #available
始终会通过return
结果分支.
func test() -> some Shape {
if #available(macOS 100, *) {
if cond { ✅
return Rectangle(...)
} else {
return Rectangle(...)
}
}
return self
}
但是如果test()
函数中if #available
内if
分支返回不同类型,则编译器无法通过:
func test() -> some Shape {
if #available(macOS 100, *) {
if cond { ❌
return Rectangle()
} else {
return Square()
}
}
return self
}
本篇提议的这种语义调整非常适合现有模型,因为它确保每个平台始终有一个通用的基础类型。