0%

Swift 中的 some 和 any 关键字

从定义出发,some 代表了不透明类型(opaque type),而 any 代表存在类型(existential type)。

如果用一句概括的话,some 代表存在某个具体类型,这个具体类型遵循了某种协议的约束,并且这个具体类型是可以在编译期确定的,而 any 代表任意一种满足某种协议约束的类型,但这个类型具体是什么,必须要在运行时才能确定。

some 关键字

some 关键字是在 Swift 5.1 跟随 SwiftUI 的发布出现的,我们最常见到的 some 的使用如下所示

1
2
3
var body: some View {
···
}

实际上在这里,some 主要起到的作用是为了减少类型定义的理解难度,body 内部可以返回任意一种 SwiftUI 定义的 View,但对外部调用者而言,其实并不关心 View 的具体类型,只要编译器确保返回值确实是 View 类型即可。因此 some View 就可以很好的表达这种 ”我不关心你是什么具体类型,只要编译器知道你的类型就可以” 的含义。

Self 约束

让我们看一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
protocol Food {}

struct Meat: Food {
typealias Unit = Int
}

struct Vegetable: Food {
typealias Unit = String
}

class Person {
func eat(_ food: Food) {
if let m = food as? Meat {
print(m)
}
if let v = food as? Vegetable {
print(v)
}
}

func test() {
eat(Meat())
eat(Vegetable())
}
}

这段代码可以编译通过,可以看到我们的 eat 函数接受一个 “Food 类型” 的参数,并在内部将其转化成不同类型的 Food,但实际上更确切的说这里接受的应该是遵循 Food 协议的类型,由于 Food 协议并没有特殊性,所以这样的代码目前是可以通过编译的。

但如果我们将 Food 修改为这样的实现

1
protocol Food: Equatable {}

可以看到,我们要求 Food 遵循 Equatable 协议,Equatable 协议的定义如下

1
2
3
public protocol Equatable {
static func == (lhs: Self, rhs: Self) -> Bool
}

它有一个静态方法需要传入类型为 Self 的参数,类似这种类型的函数参数,我们称为 Self 约束,针对这种情况,一开始定义的 eat 函数就会报错,原因是在缺乏额外信息的情况下,我们并不能确定出 food 的具体类型,如此一来,Equatable 要求的 Self 类型也无法推断出来。

可以想象一个简单的例子,如果我们在 eat 函数内部也有一个遵循 Food 类型的变量 a,那么变量 a 实际上是否能够和 food 参数判等是不能确定的,如果运行时传入的 food 和 a 的类型一样自然可以判等,但如果类型不一样就不能判等,我们就不能简单用一句 a == food 来实现我们的逻辑,这样 Equatable 协议就失去了它希望表达的两者可以判等的意图。

关联类型

接下来我们重新定义我们的 Food 协议

1
2
3
protocol Food: Equatable {
associatedtype Unit: Hashable
}

我们这里用到了关联类型,也就是 Unit,这样也会导致 eat 函数不能编译通过,原因和上面类似,因为有了关联类型 Unit 的存在,我们不能单纯从 Food 协议本身确定出具体类型,需要更多信息来确定。

反向泛型

上面两种情况我们都可以用 some 关键字来解决编译问题,也就是类似下面这样

1
2
3
4
5
6
7
8
func eat(_ food: some Food) {
if let m = food as? Meat {
print(m)
}
if let v = food as? Vegetable {
print(v)
}
}

它其实和用泛型约束实现是一个效果

1
2
3
4
5
6
7
8
func eat<F: Food>(_ food: F) {
if let m = food as? Meat {
print(m)
}
if let v = food as? Vegetable {
print(v)
}
}

但这两者又有所区别,让我们看看下面的代码

1
2
3
4
5
6
7
func a() -> some Food {
Vegetable()
}

func b<F: Food>() -> F {
F()
}

可以看到,函数 a 和 b 看起来实现了一样的效果,都是返回遵循 Food 协议的类型实例,但 b 函数返回的具体类型实际上取决于调用者,比如下面代码虽然都调用了 b 函数,但是返回值类型分别是 Vegetable 和 Meat。

1
2
let vegetable: Vegetable = b()
let meat: Meat = b()

而 some 则不一样,无论是谁调用 a 函数,a 函数返回值类型都由 a 函数自身实现决定,在这里也就是 Vegetable 类型,这种由实现方决定具体返回类型的方式,又被称为“反向泛型”(reverse generics)。

用途

由于 some 关键字所解决的问题实际上是一种特定问题,所以大部分开发场景我们大概不会用到 some 关键字,我在这里总结了一些可能的使用场景

  • 如果一个泛型参数只在一个地方使用,可以用 some 代替它以提升代码可读性,类似前面的 eat 函数
1
2
3
func eat(_ food: some Food) { }

func eat<F: Food>(_ food: F) { }
  • 作为模块的开发者,希望对外隐藏接口的返回值类型,例如上面所举的 a 函数
1
2
3
func a() -> some Food {
Vegetable()
}
  • 类型过于复杂,并且对于开发者而言没有太大意义,只需要编译器知晓类型的信息即可

例如,在 Combine 中,多个 Operator 叠加后的 Publisher 类型会非常复杂,而开发者并不需要关心具体类型,some 关键字就可以派上用场

1
2
3
4
5
6
7
func somePublisher() -> some Publisher {
let p1 = [[1, 2, 3], [4, 5, 6]]
.publisher
.flatMap { $0.publisher }
.map { $0 * 2 }
return p1
}

这里 p1 的类型为

1
Publishers.Map<Publishers.FlatMap<Publishers.Sequence<[Int], Never>, Publishers.Sequence<[[Int]], Never>>, Int>

返回值一致性

使用 some 关键字有一个需要注意的地方是,当作为返回值修饰词时,some 要求整个方法的返回值是唯一确定的类型,例如下面的代码就是不合法的。

1
2
3
4
5
6
7
func makeCollection() -> some Collection {
if Int.random(in: 0...10).isMultiple(of: 2) {
return [0]
} else {
return ["0"]
}
}

具体报错信息是

1
Function declares an opaque return type 'some Collection', but the return statements in its body do not have matching underlying types

因为这个函数的返回值类型可能是 Int 数组也可能是字符串数组,这样编译阶段就无法确定 makeCollection 的返回值类型,换句话说,some 表达的是存在且只存在一种确定的类型,假如希望返回不同类型的值,可以用到后面的 any 关键字。

当然如果是支持泛型约束的函数,也可以用 some 关键字修饰,比如下面的代码就是合法的,这是因为泛型函数本身的类型通过泛型参数 T 是可以唯一确定的。

1
2
3
func makeCollection<T>() -> some Collection {
return [T]()
}

any 关键字

any 关键字是 Swift 5.6 引入的,any 修饰的协议类型称为存在类型,也叫做盒子类型,它表达的意思是某个对象具体的类型需要在运行时阶段确定,编译期只能模糊确定它遵循某种协议,像是一个盒子,盒子里可以装任意符合条件的具体类型,也就是”存在类型”的含义。

但存在类型由于需要在运行时阶段才能确定具体类型和内存分布,因此编译器无法针对存在类型做更多有效的优化,比如泛型特化(Specialization),以及方法调用时只能采用动态派发而不是直接静态派发的方式,所以性能上会有损耗。Swift 为了让开发者意识到使用存在类型存在的这种性能损耗,所以强制让开发者在使用存在类型时加上关键字,换句话说,any 的存在并没有在编译层面起到太多作用,更多是为了提醒开发者。

这种存在类型的使用是很常见,例如下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
protocol Food { }

func eat(_ food: Food) { }

protocol Person {
associatedtype Country
}

func meet(_ person: Person) { }

protocol Unit: Equatable { }

func compare(_ a: Unit, _ b: Unit) { }

其中,meet 和 compare 函数在 Swift 5.7 都会报错

1
2
3
Use of protocol 'Person' as a type must be written 'any Person'

Use of protocol 'Unit' as a type must be written 'any Unit'

而 eat 函数虽然也用到了存在类型,但是 Swift 团队计划在 Swift 6 再提示编译错误。

any 确实可以去除上述编译报错,但是 Swift 团队的本意其实是希望尽可能少使用 any ,或者说尽可能少使用存在类型,转而用泛型和具体类型实现相同的逻辑。

我们可以将前面的代码改写为下述代码

1
2
3
4
5
6
7
8
9
10
11
12
13
protocol Food { }

func eat(_ food: some Food) { }

protocol Person {
associatedtype Country
}

func meet(_ person: some Person) { }

protocol Unit: Equatable { }

func compare<U: Unit>(_ a: U, _ b: U) { }

这会让我们的代码语义更明确,同时在编译阶段也能确定函数接受的具体类型参数。

事实上,大部分场景下,any 关键字都可以用泛型、具体类型、不透明类型来代替,功能上没有什么区别,而性能上优于存在类型,目前可以想到的只能用 any 关键字的地方,是某些需要返回不同类型的场景,例如下面的代码

1
2
3
4
5
6
7
func makeFood() -> any Food {
if ... {
return Meat()
} else {
return Vegetable()
}
}

相关链接