Swift 中的不透明类型与装箱协议类型:概念、区别与实践

前言

Swift 提供了两种隐藏类型信息的方式:不透明类型(opaque type) 和 装箱协议类型(boxed protocol type)。

它们都用于隐藏具体类型,但在类型身份、性能、灵活性等方面有本质区别。

不透明类型(Opaque Types)

基本概念

不透明类型允许函数返回一个遵循某个协议的具体类型,但调用者无法知道具体是什么类型。编译器知道类型信息,但调用者不知道。

使用关键字 some 来声明不透明类型。

swift 复制代码
protocol Shape {
    func draw() -> String
}

struct Square: Shape {
    func draw() -> String {
        return "■"
    }
}

func makeSquare() -> some Shape {
    return Square()
}

调用者知道返回的是一个 Shape,但不知道它是 Square

与泛型的区别

泛型是调用者决定类型,不透明类型是实现者决定类型。

swift 复制代码
// 泛型:调用者决定类型
func maxNum<T: Comparable>(_ x: T, _ y: T) -> T {
    // 实现者,不知道T是什么类型,只知道T可以进行比较
    return x > y ? x : y
}

// 调用者传入1,3。编译器会自动推断T为Int
// 也可以补全写法 maxNum<Int>(1, 3)
let maxInt = maxNum(1,3)


// 不透明类型:实现者决定类型
func makeShape() -> some Shape {
    // 实现者 决定最终返回的具体类型
    return Square()
}
// 下面的写法报错,调用者不能指定类型
// Cannot convert value of type 'some Shape' to specified type 'Square'
//let shape: Square = makeShape()
// 只能用some Shape
let shape: some Shape = makeShape()

不透明类型的限制:必须返回单一类型

swift 复制代码
struct FlippedShape: Shape {
    var shape: Shape
    init(_ shape:  Shape) {
        self.shape = shape
    }
    func draw() -> String {
        let lines = shape.draw().split(separator: "\n")
        return lines.reversed().joined(separator: "\n")
    }
}
// ❌ 错误:返回了不同类型
// Function declares an opaque return type 'some Shape', but the return statements in its body do not have matching underlying types
func invalidFlip<T: Shape>(_ shape: T) -> some Shape {
    if shape is Square {
        return shape // 返回的是 T
    }
    return FlippedShape(shape) // 返回的是 FlippedShape<T>
}

✅ 正确做法:统一返回类型

swift 复制代码
func flip<T: Shape>(_ shape: T) -> some Shape {
    return FlippedShape(shape) // 总是返回 FlippedShape<T>
}

泛型与不透明类型结合使用

swift 复制代码
struct RepeatedShape: Shape {
    let shape: Shape
    let count: Int
    
    func draw() -> String {
        return (0..<count).map { _ in shape.draw() }.joined(separator: "\n")
    }
}

func repeatShape<T: Shape>(shape: T, count: Int) -> some Shape {
    return RepeatedShape(shape: shape, count: count)
}

虽然 T 是泛型,但返回类型始终是 RepeatedShape<T>,满足单一类型要求。

装箱协议类型(Boxed Protocol Types)

基本概念

装箱协议类型使用 any 关键字,表示"任意遵循该协议的类型",类型信息在运行时确定,类型身份不保留。

swift 复制代码
let shape: any Shape = Square()

any Shape 可以存储任何遵循 Shape 的类型,类似于 Objective-C 的 id<Protocol>

使用示例

swift 复制代码
struct VerticalShapes {
    var shapes: [any Shape] // 可以存储不同类型的 Shape

    func draw() -> String {
        return shapes.map { $0.draw() }.joined(separator: "\n")
    }
}

类型擦除与运行时类型检查

swift 复制代码
struct Triangle: Shape {
    func draw() -> String {
        return "🔺"
    }
}

let shapes: [any Shape] = [Square(), Triangle()]

for shape in shapes {
    if let square = shape as? Square {
        print("这是一个方块:\(square.draw())")
    }
}

不透明类型 vs 装箱协议类型

特性 不透明类型 some 装箱协议类型 any
类型身份 保留(编译期已知) 不保留(运行时确定)
灵活性 低(只能返回一种类型) 高(可返回多种类型)
性能 更好(无运行时开销) 有装箱开销
是否支持协议关联类型 ✅ 支持 ❌ 不支持

代码示例对比

不透明类型版本

swift 复制代码
func flip<T: Shape>(_ shape: T) -> some Shape {
    return FlippedShape(shape)
}

let flipped = flip(Square())
let doubleFlipped = flip(flipped) // ✅ 支持嵌套

装箱协议类型版本

swift 复制代码
func protoFlip(_ shape: any Shape) -> any Shape {
    return FlippedShape(shape)
}

let flipped = protoFlip(Square())
let doubleFlipped = protoFlip(flipped)  // ✅ 支持嵌套

不透明参数类型(some 作为参数)

swift 复制代码
func drawTwice(_ shape: some Shape) {
    print(shape.draw())
    print(shape.draw())
}

等价于泛型函数:

swift 复制代码
func drawTwice<S: Shape>(_ shape: S) {
    print(shape.draw())
    print(shape.draw())
}

总结与最佳实践

使用场景 推荐类型
隐藏实现细节,返回单一类型 some Protocol
存储多种协议类型 any Protocol
需要运行时类型判断 any Protocol

在实际项目中的应用

SwiftUI 中的不透明类型

swift 复制代码
var body: some View {
    VStack {
        Text("Hello")
        Image(systemName: "star")
    }
}

body 返回的是某个具体类型(如 _VStack<TupleView<(Text, Image)>>),但调用者无需关心。

网络层返回模型抽象

swift 复制代码
protocol Model {
    static func parse(from data: Data) -> Self?
}

func fetch<T: Model>(_ type: T.Type) -> some Model {
    // 隐藏具体模型类型,返回某个遵循 Model 的类型
}

结语

不透明类型和装箱协议类型是 Swift 类型系统中非常强大的工具,它们各自解决了不同层面的抽象问题:

  • some 更适合封装实现细节,提供类型安全的抽象接口;
  • any 更适合运行时灵活性,但牺牲类型信息和性能。

理解它们的本质区别,能帮助你在设计 API、构建模块时做出更合理的架构决策。

参考资料

相关推荐
HarderCoder7 小时前
Swift 泛型深度指南 ——从“交换两个值”到“通用容器”的代码复用之路
swift
东坡肘子8 小时前
惊险但幸运,两次!| 肘子的 Swift 周报 #0109
人工智能·swiftui·swift
胖虎18 小时前
Swift项目生成Framework流程以及与OC的区别
framework·swift·1024程序员节·swift framework
songgeb1 天前
What Auto Layout Doesn’t Allow
swift
YGGP1 天前
【Swift】LeetCode 240.搜索二维矩阵 II
swift
YGGP2 天前
【Swift】LeetCode 73. 矩阵置零
swift
非专业程序员Ping3 天前
HarfBuzz 实战:五大核心API 实例详解【附iOS/Swift实战示例】
android·ios·swift
Swift社区4 天前
LeetCode 409 - 最长回文串 | Swift 实战题解
算法·leetcode·swift
YGGP6 天前
【Swift】LeetCode 54. 螺旋矩阵
swift