现在位置: 首页 > Swift 教程 > 正文

Swift some 与 any

在 Swift 里,some Pany P 都可以表示「某个遵循协议 P 的类型」,但它们的含义完全不同。

some不透明类型:对外隐藏具体类型,但编译器知道它是唯一确定的一种类型。

any存在类型:运行时可以装任何遵循 P 的类型,代价是失去静态类型信息。

本文基于 Swift 6.x,示例可直接运行。


为什么需要这两种写法

先看一个共同的需求:函数返回的东西只要满足某个协议即可,具体类型不想暴露。

Swift 提供了两条路,一条是 some,一条是 any,它们解决的是不同问题。

对比项some P(不透明类型)any P(存在类型)
具体类型编译期固定为一种运行时可以是任意遵循者
类型信息调用者看不到,编译器看得到被擦除
能否装不同类型不能可以
调用方式静态派发动态派发,有装箱开销
典型用途隐藏实现细节、SwiftUI 的 body异构集合、运行时才确定类型

下面分别展开。


不透明返回类型 some

在返回类型前写 some 协议名,就表示「我返回一个遵循该协议的类型,但具体是哪个类型不告诉你」。

调用者只能使用协议里声明的成员,看不到具体类型,也就无法依赖实现细节。

实例

import Foundation

protocol RunoobShape {
    func area() -> Double
}

struct RunoobCircle: RunoobShape {
    var radius: Double
    func area() -> Double { .pi * radius * radius }
}

struct RunoobSquare: RunoobShape {
    var side: Double
    func area() -> Double { side * side }
}

// 返回 some:调用者只知道「这是一个 RunoobShape」,具体类型被隐藏
func makeCircle(_ radius: Double) -> some RunoobShape {
    return RunoobCircle(radius: radius)
}

let circle = makeCircle(2.0)
print(String(format: "圆面积:%.4f", circle.area()))
print("静态类型是 \(type(of: circle))")

运行结果:

圆面积:12.5664
静态类型是 RunoobCircle

注意最后一行的输出:type(of:) 返回的仍然是真实的 RunoobCircle

因为 some 隐藏的是编译期静态类型,运行时对象本身还是那个具体类型,只是你在代码里写不出它的名字。

同一个函数只能返回一种类型

some 要求函数所有返回语句的底层类型完全一致,否则编译报错。

实例

protocol RunoobShape {
    func area() -> Double
}
struct RunoobCircle: RunoobShape {
    var radius: Double
    func area() -> Double { .pi * radius * radius }
}
struct RunoobSquare: RunoobShape {
    var side: Double
    func area() -> Double { side * side }
}

func makeShape(_ isCircle: Bool) -> some RunoobShape {
    if isCircle {
        return RunoobCircle(radius: 1.0)
    }
    return RunoobSquare(side: 1.0)
}

编译报错:

error: function declares an opaque return type 'some RunoobShape',
but the return statements in its body do not have matching underlying types
note: return statement has underlying type 'RunoobCircle'
note: return statement has underlying type 'RunoobSquare'

这种「可能返回多种类型」的需求,正是 any 的用武之地。


存在类型 any

any 协议名 就得到一个可以存放任意遵循者的「盒子」,盒子在运行时会记住里面到底是什么类型。

同一个变量可以先装圆、再装方,数组里也能混放不同类型。

实例

import Foundation

protocol RunoobShape {
    func area() -> Double
}

struct RunoobCircle: RunoobShape {
    var radius: Double
    func area() -> Double { .pi * radius * radius }
}

struct RunoobSquare: RunoobShape {
    var side: Double
    func area() -> Double { side * side }
}

// any:存在类型,一个变量可以先后装入不同的遵循者
var shape: any RunoobShape = RunoobCircle(radius: 1.0)
print(String(format: "面积:%.4f", shape.area()))

shape = RunoobSquare(side: 3.0)
print(String(format: "面积:%.4f", shape.area()))

// 数组里可以混放不同类型,这是 any 的典型用途
let shapes: [any RunoobShape] = [
    RunoobCircle(radius: 1.0),
    RunoobSquare(side: 2.0),
    RunoobCircle(radius: 3.0)
]

var total = 0.0
for s in shapes {
    total += s.area()
}
print(String(format: "总面积:%.4f", total))
print("元素个数:\(shapes.count)")

运行结果:

面积:3.1416
面积:9.0000
总面积:35.4159
元素个数:3

shapes 数组里同时有圆和方,用 some 是做不到的。

注意:从 Swift 5.7 起,写协议类型时必须显式加 any,例如 any RunoobShape。只写 RunoobShape 会得到一个警告,提示你改用 any 或泛型。


some 与 any 的选择

两者不是替代关系,而是各有适用场景。判断标准很简单:调用点是否需要同时处理多种具体类型。

some 与 any 对比:some 在编译期确定唯一具体类型、无装箱,调用方按具体类型使用;any 用存在类型容器在运行时存放任意遵循者,容器由缓冲区、元数据和 witness table 组成,带来间接开销

场景推荐写法原因
函数只返回一种实现,想隐藏细节some P静态派发,无开销
需要返回多种实现,由参数决定any P运行时可切换类型
数组、字典里混放不同遵循者any P异构集合只能用存在类型
写通用算法,类型由调用方决定泛型 <T: P>保留完整类型信息,性能最好

经验法则:能用泛型就用泛型,需要隐藏实现就用 some,确实需要运行时多态才用 any

SwiftUI 里的 var body: some View 就是典型的不透明类型用法:每个视图的 body 返回一种具体视图,但对外只暴露 View 协议。


主关联类型

带关联类型的协议,可以在协议名后的尖括号里声明「主关联类型」,之后就能把关联类型直接写在类型表达式里。

Collection 的主关联类型是 Element,所以可以写 Collection<Int>

实例

import Foundation

// Collection 的主关联类型是 Element,可以用尖括号直接写约束
func runoobFirstInt<C: Collection<Int>>(_ values: C) -> Int? {
    return values.first
}

print(runoobFirstInt([10, 20, 30]) ?? -1)

// 返回 some Collection<Int>:只暴露「元素是 Int 的集合」,隐藏具体容器类型
func runoobNumbers() -> some Collection<Int> {
    return [1, 2, 3, 4, 5]
}

let numbers = runoobNumbers()
print("元素个数:\(numbers.count)")
print("第一个元素:\(numbers.first!)")

// any Collection<Int>:存在类型,可存放不同的集合实现
var anyCollection: any Collection<Int> = [1, 2, 3]
print("元素个数:\(anyCollection.count)")

anyCollection = Set([1, 1, 2, 3])
print("去重后个数:\(anyCollection.count)")

运行结果:

10
元素个数:5
第一个元素:1
元素个数:3
去重后个数:3

自定义协议也能声明主关联类型,只要在协议名后用尖括号标出即可。

实例

// 声明 Item 为主关联类型
protocol RunoobContainer<Item> {
    associatedtype Item
    var count: Int { get }
}

// 之后就可以写 RunoobContainer<Int> 这样的约束
func runoobCount<C: RunoobContainer<Int>>(_ container: C) -> Int {
    return container.count
}

struct RunoobIntBox: RunoobContainer {
    typealias Item = Int
    var count: Int
}

print(runoobCount(RunoobIntBox(count: 7)))

运行结果:

7

遵循者必须让 Item 与约束里的 Int 一致,否则编译器会报「类型不相等」。


常见问题

最后回答几个实际编码中容易卡住的问题。

some 能不能用在属性上

可以用于计算属性,比如 var body: some View;但不能用来声明存储属性,因为存储属性需要确切的类型和内存布局。

any 会不会有性能问题

会有一定开销。存在类型需要装箱,方法调用走动态派发;如果在热点代码里频繁调用,应优先考虑泛型或 some

any 里怎么拿回具体类型

as? 向下转型,判断成功后再访问具体类型独有的成员,详见《Swift 类型转换》。

为什么 SwiftUI 的 body 要用 some 而不是 any

因为每个视图的 body 只返回一种具体视图类型,用 some 可以避免装箱、走静态派发,同时不把实现细节暴露给使用者。