Swift5.7 扩展不透明类型(some)到泛型参数

2022-07-03 16:13:44 浏览数 (3)

介绍

Swift 中的泛型语法是为了类型通用性设计,这种通用性允许在函数输入和输出时,使用复杂的类型集合来表达,前提是类型必须前后一致。例如下面这个例子是从两个序列构建一个数组:

代码语言:Swift复制
func eagerConcatenate<Sequence1: Sequence, Sequence2: Sequence>(
  _ sequence1: Sequence1, _ sequence2: Sequence2
) -> [Sequence1.Element] where Sequence1.Element == Sequence2.Element

这个函数声明中有不少内容:两个函数参数是由调用者确定的不同类型,分别由 Sequence1Sequence2来捕获。这两个类型都需要遵守Sequence协议,而且,where条件语句决定两个序列中的元素也必须是相同类型。最后, 该函数的返回值是Sequence1中元素类型组成的数组。只要满足上述约束条件,就可以将此操作用于许多不用的输入,例如:

代码语言:Swift复制
eagerConcatenate([1, 2, 3], Set([4, 5, 6]))  // okay, produces an [Int]
eagerConcatenate([1: "Hello", 2: "World"], [(3, "Swift"), (4, "!")]) // okay, produces an [(Int, String)]
eagerConcatenate([1, 2, 3], ["Hello", "World"]) // error: sequence element types do not match

那么这种语法有没有可能做个简化?对于不需要引入这些复杂约束的场景,这种语法就显的比较重。比如,下面这个函数描述在水平方向组合两个 SwiftUI 中的视图:

代码语言:Swift复制
func horizontal<V1: View, V2: View>(_ v1: V1, _ v2: V2) -> some View {
  HStack {
    v1
    v2
  }
}

有很多模版文件可以声明只使用一次的泛型参数类型 V1V2, 在上面例子中的模版是<V1: View, V2: View>。因为 V1V2 并不是它真正的类型,还需要往模版里找到它真实的类型定义。这实际上比它本身看起来更复杂(会比平常的调用理解多一层, 当前的参数寻找过程是:v1 -> V1 -> View, 而非 v1 -> View)。对返回的结果来说,又可以使用不透明类型(opaque result type)some来隐藏实际的返回值,仅通过它符合的协议来描述它。

本篇提议把不透明类型的语法扩展到了参数上,允许指定泛型函数参数,而不需要声明与泛型参数列表关联的模版。此时上述的 horizontal 函数可以省略模版声明 <V1: View, V2: View>, 可以这样写:

代码语言:Swift复制
func horizontal(_ v1: some View, _ v2: some View) -> some View {
  HStack {
    v1
    v2
  }
}

改进后的horizontal函数本质上与第一个表达等价,但是改进后的函数去掉了泛型参数和模版之前对应关系,更加方便阅读和理解:它接受两个视图(视图的具体类型这里不重要),并返回一个视图(返回的视图类型也不重要)。

提议的解决方案

这篇提议把some关键字的用法扩展到函数,初始化器(initializer)和下标声明的参数类型中。与不透明类型一样,some P表示的类型没有名字,只有一个遵守协议P的约束。当某个参数类型内出现了一个不透明类型时,这个不透明类型会被没有名字的泛型参数代替。举个例子:

代码语言:Swift复制
func f(_ p: some P) { }

与下面的例子是等价的。此时参数p表示一个遵循协议P的任何类型。

代码语言:Swift复制
func f<_T: P>(_ p: _T) { }

与不透明结果类型不同,调用方通过类型推断确定不透明参数类型的真实类型。例如,我们假设IntString都遵循协议P,则可以使用IntString来完成函数调用,或者引用函数:

代码语言:Swift复制
f(17) // ✅,推断不透明类型为 Int
f("Hello") // ✅,推断不透明类型为 String

let fInt: (Int) -> Void = f     // ✅,推断不透明类型为 Int  (f 函数无返回值,与返回 Void 等价)
let fString: (String) -> Void = f // ✅,推断不透明类型为 String

SE-0328 这篇提议是讲结构化的不透明结果类型,它扩展了不透明结果类型,允许在结果类型中的任何结构位置多次使用some P类型。参数中的不透明类型也允许相同的结构用法,例如:

代码语言:Swift复制
func encodeAnyDictionaryOfPairs(_ dict: [some Hashable & Codable: Pair<some Codable, some Codable>]) -> Data

上述用法跟下面等价(分析:泛型参数_T1,_T2,_T3 和它们对应的模版 <_T1: Hashable & Codable, _T2: Codable, _T3: Codable>

代码语言:Swift复制
func encodeAnyDictionaryOfPairs<_T1: Hashable & Codable, _T2: Codable, _T3: Codable>(_ dict: [_T1: Pair<_T2, _T3>]) -> Data

声明中some修饰的每一个实例都代表每个不同的隐式泛型参数。

不透明结果类型和不透明参数类型其实很相似,都是使用some 关键字来修饰,前者用在返回结果中,后者用在参数中。本质都是表达遵循同一协议类型的泛型类型。

详细设计实现

不透明参数类型只能用于函数,初始化器(initializer), 和下标声明中的参数修饰,不能把它们用作别名(typealias),或者函数类型中的入参(function type)。例如:

代码语言:Swift复制
typealias Fn = (some P) -> Void    // error: 不能用作别名
let g: (some P) -> Void = f           // error: 不能用于函数类型的参数值

除了别名和函数类型中参数值这两个限制。还有2个场景限制使用:可变泛型和函数类型的参数。

可变泛型

不透明类型不能在可变参数中使用。比如下例中的可变参数 P...,不能使用some类型:

代码语言:Swift复制
func acceptLots(_: some P...)

这个限制之所以存在,是因为如果 Swift 获得可变泛型,则当前提议所实现的效果就会不成立。继续看上面这个例子,上述函数在当前提议的规则下,some P...该语法糖对应的真实表达应该是<_T: P>(_: _T...), 跟下面的表达等价:

代码语言:Swift复制
func acceptLots<_T: P>(_: _T...)

由于这里支持可变参数,并且可变参数的类型都要求一样,明显调用函数传入不同参数时,会报错:

代码语言:Swift复制
acceptLots(1, 1, 2, 3, 5, 8)          // okay
acceptLots("Hello", "Swift", "World") // okay
acceptLots("Swift", 6)                // error: argument for `some P` could be either String or Int

可以看出当前提议规则生成<_T: P>是支持相同类型的泛型,如果支持可变泛型,则函数允许不同类型的输入,前后不一致无法兼容。

针对上述不同参数的报错,有一种可能的解决方案是:对于可变泛型,可以将隐式泛型参数改为泛型参数包,也就是模版中P改为P...,此时约束从遵循同一类型的泛型变成支持不同类型的泛型(感觉支持了所有类型?,已经感知不到约束):

代码语言:Swift复制
func acceptLots<_ Ts: P...>(_: _Ts...)

这时,acceptLots可以接受各种不同类型的参数:

代码语言:Swift复制
acceptLots(1, 1, 2, 3, 5, 8)          // okay, Ts 包括 6个 Int 参数
acceptLots("Hello", "Swift", "World") // okay, Ts 包括 3个 String 参数
acceptLots("Swift", 6)                  // okay,  Ts 包括 1个 String 参数和1个 Int 参数

当前提议不包括这种解决方案,社区提出了这种解决方案,可能在后续的版本会考虑。

函数类型的参数中使用不透明参数

SE-0328 禁止在函数类型的参数中使用不透明参数。例如函数f()返回值是函数类型 (some P) -> Void

代码语言:Swift复制
func f() -> (some P) -> Void {  ...  }     // ❌,不能在函数类型的参数中使用不透明参数 some

然后我们再按正常使用调用 f(), 把f()的结果赋值给fn, 例如:

代码语言:Swift复制
let fn = f()
fn(/* 这里应该怎么构造函数中的值?这里不知道怎么写 */)

很显然在调用fn函数时,很难使用。因为调用者无法轻松创建未知的,未命名类型的参数值。

相同的规则也运用在函数类型作为参数的情况。其实本质还是 some P 不能作为函数类型中的参数类型。例如:

代码语言:Swift复制
func g(fn: (some P) -> Void {  ...  }   // ❌,不能在函数类型的参数中使用不透明参数

在函数 g 的实现过程中,如果some P类型的值在其他地方没有命名,则很难生成该类型的值。

对源代码兼容性影响

当前提议特性是一个纯语言扩展,没有向后兼容性问题,因为some在参数上的所有使用,目前正在其他版本都会报错。

对 ABI 稳定性影响

不影响 ABI 和运行时,因为some本质上是泛型的语法糖。

对 API 扩展性影响

不会破坏 ABI 或者 API。some是语法糖,表达的是带模版的显式泛型参数(回忆下最初的目的是想把:<V1: View, V2: View>(_ v1: V1, _ v2: V2) 转为 (_ v1: some View, _ v2: some View) 写法)。也就是与现有的这种语法是等价的,但在从 Swift 5.7 你可以使用更为简洁的 some P 来修饰参数,而非仅仅是返回结果。唯一的前提是前后写法的约束类型必须相同。

总结

通过当前提议 SE-0341,你应当知道:

  1. Swift5.7 通过运用 some 到泛型参数类型,是为了去除泛型模块声明的冗余表达;
  2. some 对应的是与之等价的泛型模版表达式;
  3. 内部通过类型推断,确定真实的不透明参数类型所对应的类型

0 人点赞