从定义出发,some 代表了不透明类型(opaque type),而 any 代表存在类型(existential type)。
如果用一句概括的话,some 代表存在某个具体类型,这个具体类型遵循了某种协议的约束,并且这个具体类型是可以在编译期确定的,而 any 代表任意一种满足某种协议约束的类型,但这个类型具体是什么,必须要在运行时才能确定。
some 关键字
some 关键字是在 Swift 5.1 跟随 SwiftUI 的发布出现的,我们最常见到的 some 的使用如下所示
1 | var body: some View { |
实际上在这里,some 主要起到的作用是为了减少类型定义的理解难度,body 内部可以返回任意一种 SwiftUI 定义的 View,但对外部调用者而言,其实并不关心 View 的具体类型,只要编译器确保返回值确实是 View 类型即可。因此 some View 就可以很好的表达这种 ”我不关心你是什么具体类型,只要编译器知道你的类型就可以” 的含义。
Self 约束
让我们看一段代码
1 | protocol Food {} |
这段代码可以编译通过,可以看到我们的 eat 函数接受一个 “Food 类型” 的参数,并在内部将其转化成不同类型的 Food,但实际上更确切的说这里接受的应该是遵循 Food 协议的类型,由于 Food 协议并没有特殊性,所以这样的代码目前是可以通过编译的。
但如果我们将 Food 修改为这样的实现
1 | protocol Food: Equatable {} |
可以看到,我们要求 Food 遵循 Equatable 协议,Equatable 协议的定义如下
1 | public protocol Equatable { |
它有一个静态方法需要传入类型为 Self 的参数,类似这种类型的函数参数,我们称为 Self 约束,针对这种情况,一开始定义的 eat 函数就会报错,原因是在缺乏额外信息的情况下,我们并不能确定出 food 的具体类型,如此一来,Equatable 要求的 Self 类型也无法推断出来。
可以想象一个简单的例子,如果我们在 eat 函数内部也有一个遵循 Food 类型的变量 a,那么变量 a 实际上是否能够和 food 参数判等是不能确定的,如果运行时传入的 food 和 a 的类型一样自然可以判等,但如果类型不一样就不能判等,我们就不能简单用一句 a == food
来实现我们的逻辑,这样 Equatable 协议就失去了它希望表达的两者可以判等的意图。
关联类型
接下来我们重新定义我们的 Food 协议
1 | protocol Food: Equatable { |
我们这里用到了关联类型,也就是 Unit,这样也会导致 eat 函数不能编译通过,原因和上面类似,因为有了关联类型 Unit 的存在,我们不能单纯从 Food 协议本身确定出具体类型,需要更多信息来确定。
反向泛型
上面两种情况我们都可以用 some 关键字来解决编译问题,也就是类似下面这样
1 | func eat(_ food: some Food) { |
它其实和用泛型约束实现是一个效果
1 | func eat<F: Food>(_ food: F) { |
但这两者又有所区别,让我们看看下面的代码
1 | func a() -> some Food { |
可以看到,函数 a 和 b 看起来实现了一样的效果,都是返回遵循 Food 协议的类型实例,但 b 函数返回的具体类型实际上取决于调用者,比如下面代码虽然都调用了 b 函数,但是返回值类型分别是 Vegetable 和 Meat。
1 | let vegetable: Vegetable = b() |
而 some 则不一样,无论是谁调用 a 函数,a 函数返回值类型都由 a 函数自身实现决定,在这里也就是 Vegetable 类型,这种由实现方决定具体返回类型的方式,又被称为“反向泛型”(reverse generics)。
用途
由于 some 关键字所解决的问题实际上是一种特定问题,所以大部分开发场景我们大概不会用到 some 关键字,我在这里总结了一些可能的使用场景
- 如果一个泛型参数只在一个地方使用,可以用 some 代替它以提升代码可读性,类似前面的 eat 函数
1 | func eat(_ food: some Food) { } |
- 作为模块的开发者,希望对外隐藏接口的返回值类型,例如上面所举的 a 函数
1 | func a() -> some Food { |
- 类型过于复杂,并且对于开发者而言没有太大意义,只需要编译器知晓类型的信息即可
例如,在 Combine 中,多个 Operator 叠加后的 Publisher 类型会非常复杂,而开发者并不需要关心具体类型,some 关键字就可以派上用场
1 | func somePublisher() -> some Publisher { |
这里 p1 的类型为
1 | Publishers.Map<Publishers.FlatMap<Publishers.Sequence<[Int], Never>, Publishers.Sequence<[[Int]], Never>>, Int> |
返回值一致性
使用 some 关键字有一个需要注意的地方是,当作为返回值修饰词时,some 要求整个方法的返回值是唯一确定的类型,例如下面的代码就是不合法的。
1 | func makeCollection() -> some Collection { |
具体报错信息是
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 | func makeCollection<T>() -> some Collection { |
any 关键字
any 关键字是 Swift 5.6 引入的,any 修饰的协议类型称为存在类型,也叫做盒子类型,它表达的意思是某个对象具体的类型需要在运行时阶段确定,编译期只能模糊确定它遵循某种协议,像是一个盒子,盒子里可以装任意符合条件的具体类型,也就是”存在类型”的含义。
但存在类型由于需要在运行时阶段才能确定具体类型和内存分布,因此编译器无法针对存在类型做更多有效的优化,比如泛型特化(Specialization),以及方法调用时只能采用动态派发而不是直接静态派发的方式,所以性能上会有损耗。Swift 为了让开发者意识到使用存在类型存在的这种性能损耗,所以强制让开发者在使用存在类型时加上关键字,换句话说,any 的存在并没有在编译层面起到太多作用,更多是为了提醒开发者。
这种存在类型的使用是很常见,例如下面的代码
1 | protocol Food { } |
其中,meet 和 compare 函数在 Swift 5.7 都会报错
1 | Use of protocol 'Person' as a type must be written 'any Person' |
而 eat 函数虽然也用到了存在类型,但是 Swift 团队计划在 Swift 6 再提示编译错误。
any 确实可以去除上述编译报错,但是 Swift 团队的本意其实是希望尽可能少使用 any ,或者说尽可能少使用存在类型,转而用泛型和具体类型实现相同的逻辑。
我们可以将前面的代码改写为下述代码
1 | protocol Food { } |
这会让我们的代码语义更明确,同时在编译阶段也能确定函数接受的具体类型参数。
事实上,大部分场景下,any 关键字都可以用泛型、具体类型、不透明类型来代替,功能上没有什么区别,而性能上优于存在类型,目前可以想到的只能用 any 关键字的地方,是某些需要返回不同类型的场景,例如下面的代码
1 | func makeFood() -> any Food { |
相关链接
- Some keyword in Swift: Opaque types explained with code examples
- Understanding the “some” and “any” keywords in Swift 5.7 - Swift Senpai
- 【译】Understanding the “some” and “any” keywords in Swift 5.7 - 掘金
- What is the “some” keyword in Swift? – Donny Wals
- What is the “any” keyword in Swift? – Donny Wals
- 【译】What is the “any” keyword in Swift? - 掘金
- What are primary associated types in Swift 5.7? – Donny Wals
- swift-evolution/0346-light-weight-same-type-syntax.md at main · apple/swift-evolution · GitHub
- 【Swift-不透明类型】从SwiftUI看some关键字 - 掘金