Swift编程中的不透明类型与自动引用计数
立即解锁
发布时间: 2025-08-16 02:36:03 阅读量: 14 订阅数: 30 AIGC 


Swift编程入门与实践指南
# Swift编程中的不透明类型与自动引用计数
## 1. 不透明类型概述
在编程中,函数或方法的不透明返回类型会隐藏其返回值的类型信息。它不提供具体的类型作为返回类型,而是依据返回值所支持的协议来描述。这种隐藏类型信息的方式在模块与调用该模块的代码之间的边界处非常有用,因为返回值的底层类型可以保持私有。与返回协议类型的值不同,不透明类型保留了类型标识,编译器可以访问类型信息,但模块的客户端无法访问。
### 1.1 不透明类型解决的问题
假设我们要编写一个绘制 ASCII 艺术形状的模块。ASCII 艺术形状的基本特征是有一个 `draw()` 函数,该函数返回该形状的字符串表示。我们可以将其作为 `Shape` 协议的要求:
```swift
protocol Shape {
func draw() -> String
}
struct Triangle: Shape {
var size: Int
func draw() -> String {
var result: [String] = []
for length in 1...size {
result.append(String(repeating: "*", count: length))
}
return result.joined(separator: "\n")
}
}
let smallTriangle = Triangle(size: 3)
print(smallTriangle.draw())
// *
// **
// ***
```
我们可以使用泛型来实现垂直翻转形状等操作,如下所示:
```swift
struct FlippedShape<T: Shape>: Shape {
var shape: T
func draw() -> String {
let lines = shape.draw().split(separator: "\n")
return lines.reversed().joined(separator: "\n")
}
}
let flippedTriangle = FlippedShape(shape: smallTriangle)
print(flippedTriangle.draw())
// ***
// **
// *
```
定义一个 `JoinedShape` 结构体来垂直连接两个形状,会导致类型信息泄露。例如,将一个翻转的三角形与另一个三角形连接会产生 `JoinedShape<FlippedShape<Triangle>, Triangle>` 这样的类型。
```swift
struct JoinedShape<T: Shape, U: Shape>: Shape {
var top: T
var bottom: U
func draw() -> String {
return top.draw() + "\n" + bottom.draw()
}
}
let joinedTriangles = JoinedShape(top: smallTriangle, bottom: flippedTriangle)
print(joinedTriangles.draw())
// *
// **
// ***
// ***
// **
// *
```
这种暴露形状创建详细信息的方式会导致一些本不应成为 ASCII 艺术模块公共接口一部分的类型泄露,因为需要声明完整的返回类型。模块内部的代码可以通过多种方式构建相同的形状,而模块外部使用该形状的代码不应考虑这些转换列表的实现细节。像 `JoinedShape` 和 `FlippedShape` 这样的包装类型对模块的用户来说并不重要,也不应可见。模块的公共接口应包括连接和翻转形状等操作,这些操作返回另一个 `Shape` 值。
### 1.2 返回不透明类型
不透明类型可以看作是泛型类型的反向。泛型类型允许调用函数的代码以一种与函数实现无关的方式为函数的参数和返回值选择类型。例如:
```swift
func max<T>(_ x: T, _ y: T) -> T where T: Comparable { ... }
```
调用 `max(_:_:)` 的代码选择 `x` 和 `y` 的值,这些值的类型决定了 `T` 的具体类型。调用代码可以使用任何符合 `Comparable` 协议的类型。函数内部的代码以通用的方式编写,以便处理调用者提供的任何类型。`max(_:_:)` 的实现仅使用所有 `Comparable` 类型共享的功能。
对于具有不透明返回类型的函数,角色则相反。不透明类型允许函数实现以一种与调用函数的代码无关的方式为其返回的值选择类型。例如:
```swift
struct Square: Shape {
var size: Int
func draw() -> String {
let line = String(repeating: "*", count: size)
let result = Array<String>(repeating: line, count: size)
return result.joined(separator: "\n")
}
}
func makeTrapezoid() -> some Shape {
let top = Triangle(size: 2)
let middle = Square(size: 2)
let bottom = FlippedShape(shape: top)
let trapezoid = JoinedShape(
top: top,
bottom: JoinedShape(top: middle, bottom: bottom)
)
return trapezoid
}
let trapezoid = makeTrapezoid()
print(trapezoid.draw())
// *
// **
// **
// **
// **
// *
```
`makeTrapezoid()` 函数声明其返回类型为 `some Shape`,这意味着函数返回一个符合 `Shape` 协议的给定类型的值,但不指定任何特定的具体类型。这样编写 `makeTrapezoid()` 可以表达其公共接口的基本方面——返回的值是一个形状,而不必将形状所由组成的具体类型作为其公共接口的一部分。此实现使用了两个三角形和一个正方形,但该函数可以用多种其他方式重写以绘制梯形,而不改变其返回类型。
我们还可以将不透明返回类型与泛型结合使用:
```swift
func flip<T: Shape>(_ shape: T) -> some Shape {
return FlippedShape(shape: shape)
}
func join<T: Shape, U: Shape>(_ top: T, _ bottom: U) -> some Shape {
JoinedShape(top: top, bottom: bottom)
}
let opaqueJoinedTriangles = join(smallTriangle, flip(smallTriangle))
print(opaqueJoinedTriangles.draw())
// *
// **
// ***
// ***
// **
// *
```
这里的 `opaqueJoinedTriangles` 的值与前面泛型示例中的 `joinedTriangles` 相同。但不同的是,`flip(_:)` 和 `join(_:_:)` 将泛型形状操作返回的底层类型包装在不透明返回类型中,从而防止这些类型可见。这两个函数都是泛型的,因为它们依赖的类型是泛型的,函数的类型参数传递了 `FlippedShape` 和 `JoinedShape` 所需的类型信息。
如果具有不透明返回类型的函数从多个位置返回,所有可能的返回值必须具有相同的类型。对于泛型函数,该返回类型可以使用函数的泛型类型参数,但仍然必须是单一类型。例如,下面的形状翻转函数的无效版本包含了对正方形的特殊处理:
```swift
func invalidFlip<T: Shape>(_ shape: T) -> some Shape {
if shape is Square {
return shape // Error: return types don't match
}
return FlippedShape(shape: shape) // Error: return types don't match
}
```
如果用 `Square` 调用此函数,它返回 `Square`;否则,返回 `FlippedShape`。这违反了只返回单一类型值的要求,使 `invalidFlip(_:)` 成为无效代码。修复 `invalidFlip(_:)` 的一种方法是将正方形的特殊处理移到 `FlippedShape` 的实现中,这样该函数就可以始终返回 `FlippedShape` 值:
```swift
struct FlippedShape<T: Shape>: Shape {
var shape: T
func draw() -> String {
if shape is Square {
return shape.draw()
}
let lines = shape.draw().split(separator: "\n")
return lines.reversed().joined(separator: "\n")
}
}
```
始终返回单一类型的要求并不妨碍在不透明返回类型中使用泛型。例如:
```swift
func `repeat`<T: Shape>(shape: T, count: Int) -> some Collection {
return Array<T>(repeating: shape, count: count)
}
```
在这种情况下,返回值的底层类型根据 `T` 而变化:无论传入什么形状,`repeat(shape:count:)` 都会创建并返回该形状的数组。不过,返回值始终具有相同的底层类型 `[T]`,因此它遵循具有不透明返回类型的函数必须只返回单一类型值的要求。
### 1.3 不透明类型与协议类型的区别
返回不透明类型看起来与使用协议类型作为函数的返回类型非常相似,但这两种返回类型在是否保留类型标识方面有所不同。不透明类型指的是一个特定的类型,尽管函数的调用者无法看到是哪种类型;协议类型可以指任何符合该协议的类型。一般来说,协议类型在存储值的底层类型方面提供了更大的灵活性,而不透明类型可以对这些底层类型做出更强的保证。
例如,下面是一个使用协议类型作为返回类型而不是不透明返回类型的 `flip(_:)` 版本:
```swift
func protoFlip<T: Shape>(_ shape: T) -> Shape {
return FlippedShape(shape: shape)
}
```
这个 `protoFlip(_:)` 版本的主体与 `flip(_:)` 相同,并且总是返回相同类型的值。但与 `flip(_:)` 不同的是,`protoFlip(_:)` 返回的值不要求总是具有相同的类型,它只需要符合 `Shape` 协议。换句话说,`protoFlip(_:)` 与调用者签订的 API 契约比 `flip(_:)` 宽松得多。它保留了返回多种类型值的灵活性:
```swift
func protoFlip<T: Shape>(_ shape: T) -> Shape {
if shape is Square {
return shape
}
return FlippedShape(shape: shape)
}
```
修改后的代码根据传入的形状返回 `Square` 实例或 `FlippedShape` 实例。该函数返回的两个翻转形状可能具有完全不同的类型。此函数的其他有效版本在翻转同一个形状的多个实例时可能返回不同类型的值。`protoFlip(_:)` 返回的不太具体的类型信息意味着许多依赖类型信息的操作在返回的值上不可用。例如,无法编写一个 `==` 运算符来比较该函数返回的结果:
```swift
let protoFlippedTriangle = protoFlip(smallTriangle)
let sameThing = protoFlip(smallTriangle)
protoFlippedTriangle == sameThing // Error
```
此示例最后一行的错误有几个原因。直接的问题是 `Shape` 协议要求中不包括 `==` 运算符。如果尝试添加一个,接下来会遇到的问题是 `==` 运算符需要知道其左右参数的类型。这种运算符通常接受 `Self` 类型的参数,与采用该协议的任何具体类型相匹配,但向协议添加 `Self` 要求不允许在将协议用作类型时发生的类型擦除。
使用协议类型作为函数的返回类型可以灵活地返回任何符合该协议的类型。然而,这种灵活性的代价是一些操作在返回的值上不可用。示例显示了 `==` 运算符不可用,因为它依赖于使用协议类型时未保留的特定类型信息。
另一个问题是这种方法的形状转换不能嵌套。翻转三角形的结果是 `Shape` 类型的值,而 `protoFlip(_:)` 函数接受一个符合 `Shape` 协议的某种类型的参数。然而,协议类型的值不符合该协议,`protoFlip(_:)` 返回的值不符合 `Shape`。这意味着像 `protoFlip(protoFlip(smallTriange))` 这样应用多个转换的代码是无效的,因为翻转后的形状不是 `protoFlip(_:)` 的有效参数。
相比之下,不透明类型保留了底层类型的标识。Swift 可以推断关联类型,这使得可以在协议类型不能用作返回值的地方使用不透明返回值。例如,下面是泛型中的 `Container` 协议的一个版本:
```swift
protocol Container {
associatedtype Item
var count: Int { get }
subscript(i: Int) -> Item { get }
}
extension Array: Container { }
```
不能将 `Container` 用作函数的返回类型,因为该协议有一个关联类型。也不能将其用作泛型返回类型的约束,因为函数体外部没有足够的信息来推断泛型类型需要是什么。
```swift
// Error: Protocol with associated types can't be used as a return type.
func makeProtocolContainer<T>(item: T) -> Container {
return [item]
}
// Error: Not enough information to infer C.
func makeProtocolContainer<T, C: Container>(item: T) -> C {
return [item]
}
```
使用不透明类型 `some Container` 作为返回类型表达了所需的 API 契约——函数返回一个容器,但不指定容器的类型:
```swift
func makeOpaqueContainer<T>(item: T) -> some Container {
return [item]
}
let opaqueContainer = makeOpaqueContainer(item: 12)
let twelve = opaqueContainer[0]
print(type(of: twelve))
// Prints "Int"
```
`twelve` 的类型被推断为 `Int`,这说明了类型推断适用于不透明类型。在 `makeOpaqueContainer(item:)` 的实现中,不透明容器的底层类型是 `[T]`。在这种情况下,`T` 是 `Int`,所以返回值是一个整数数组,`Item` 关联类型被推断为 `Int`。`Container` 上的下标返回 `Item`,这意味着 `twelve` 的类型也被推断为 `Int`。
### 1.4 不透明类型相关总结
| 类型 | 特点 | 示例 |
| ---- | ---- | ---- |
| 不透明类型 | 保留类型标识,调用者不可见底层类型,返回单一类型 | `func makeTrapezoid() -> some Shape` |
| 协议类型 | 可指任何符合协议的类型,灵活性高,部分操作受限 | `func protoFlip<T: Shape>(_ shape: T) -> Shape` |
### 1.5 不透明类型工作流程 mermaid 图
```mermaid
graph LR
A[调用函数] --> B{函数返回不透明类型}
B --> C
```
0
0
复制全文
相关推荐








