泛型语法改进第一弹 —— Opaque Result Types

SE-0244 Opaque Result Types 提案前一段时间通过了 review 并且在 Swift 5.1 里完成了实现,我最早阅读这份提案的时候理解不是很透彻,今天比较仔细地读了这篇 Improving the UI of generics 之后有了更多的认识,而且发现自己之前发的 tweet 里有一些错误的认知,所以这里写篇文章,希望用最直白的方式解释清楚提案的内容,跟大家分享一下我自己的理解。

Opaque Result Types?

用最最简单的一句话来介绍这个提案的内容,就是它能让被调用方选择泛型返回值的具体类型。

这是什么意思呢?让我们来看目前最常见的泛型函数声明:

protocol Shape { ... }

func generic<T: Shape>() -> T { ... }

let x: Rectangle = generic() // type(of: x) == Rectangle, 调用者决定返回值类型

这样的声明很好,但有时候我们不希望暴露出具体的返回类型,也不想让调用者去依赖具体的类型,之前我们可以直接使用泛型类型:

func generic() -> Shape { ... }

let x = generic() // type(of: x) == Shape

虽然这样确实能达成我们的目的,但也会带来一些副作用,例如说性能问题,因为实际上 generic 返回的是一个实例的容器。

我们更希望的是能够像第一种声明那样,在编译时就确定返回值的具体类型,并且由被调用方去决定:

func reverseGeneric() -> some Shape { return Rectangle(...)  }

let x = reverseGeneric()
// type(of: x) == Rectangle
// 并且 x 的类型根据 reverseGeneric 的具体实现决定

通过引入 some 这个关键字去修饰返回值,就可以让被调用方选择具体的返回值类型,并且是在编译时确定下来的,这意味着我们不需要额外的容器去存放返回的实际值。

另外它还可以作为属性使用:

func someNumber() -> some Numeric { ... }

var number: some Numeric = someNumber()

它可以?

在这里我们需要先确立一个条件,通过 some 修饰的类型,都会在编译时确定下来,可以简单地理解为被调用方负责传入的一个泛型参数,相关的功能和限制都是基于这个特性延伸出来的。

根据前面的条件,如果返回值使用了 some 修饰,编译器可以推导出两件很重要的事情:

  1. 同一个函数签名,返回值的类型肯定也只能是同一个具体类型
  2. 外部不能够依赖函数实现里使用的返回值具体类型

这意味着什么?在之前,两个遵循了 Equatable 的实例不能判断是否相等,因为我们并不知道它们具体的类型是否一样,但如果使用了 some,并且是由同一个函数返回的,那就完全不是问题了:

func randomNumber() -> some Equatable {
let i: Int = 32
return i
}

let x = randomNumber()
let y = randomNumber()

// 在这里使用 == 是没问题的
// 因为 x 跟 y 都是由 randomNumber 返回的
// 所以它们的具体类型必然一致
print(x == y)

但是控制返回值类型的是调用者,所以你不能够依赖被调用方的具体实现:

func randomNumber() -> some Equatable {
let i: Int = 32
return i
}

var otherNumber: Int = 38
var x = randomNumber()
x = otherNumber // error

虽然你知道 randomEquatableNumber 的实现里使用的是 Int,但具体的实现有可能随时被调整,所以你不能依赖它。

结语

在这里只是简单地介绍了一些比较核心的内容,想了解更多细节的朋友可以看提案。实际上这个提案只是整个泛型语法改进计划里的第一步,之后等我有了更加深入的了解再做更多分享。