# 不透明類型
具有不透明返回類型的函數或方法會隱藏返回值的類型信息。函數不再提供具體的類型作為返回類型,而是根據它支持的協議來描述返回值。在處理模塊和調用代碼之間的關系時,隱藏類型信息非常有用,因為返回的底層數據類型仍然可以保持私有。而且不同于返回協議類型,不透明類型能保證類型一致性 —— 編譯器能獲取到類型信息,同時模塊使用者卻不能獲取到。
## 不透明類型解決的問題 {#the-problem-that-opaque-types-solve}
舉個例子,假設你正在寫一個模塊,用來繪制 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<T: Shape, U: Shape>` 結構體,能將幾何圖形垂直拼接起來。如果拼接一個翻轉三角形和一個普通三角形,它就會得到類似于 `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` 類型的值。
## 返回不透明類型 {#returning-an-opaque-type}
你可以認為不透明類型和泛型相反。泛型允許調用一個方法時,為這個方法的形參和返回值指定一個與實現無關的類型。舉個例子,下面這個函數的返回值類型就由它的調用者決定:
```swift
func max<T>(_ x: T, _ y: T) -> T where T: Comparable { ... }
```
`x` 和 `y` 的值由調用 `max(_:_:)` 的代碼決定,而它們的類型決定了 `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()` 函數可以表明它公共接口的基本性質 —— 返回的是一個幾何圖形 —— 而不是部分的公共接口生成的特殊類型。上述實現過程中使用了兩個三角形和一個正方形,還可以用其他多種方式重寫畫梯形的函數,都不必改變返回類型。
這個例子凸顯了不透明返回類型和泛型的相反之處。`makeTrapezoid()` 中代碼可以返回任意它需要的類型,只要這個類型是遵循 `Shape` 協議的,就像調用泛型函數時可以使用任何需要的類型一樣。這個函數的調用代碼需要采用通用的方式,就像泛型函數的實現代碼一樣,這樣才能讓 `makeTrapezoid()` 返回的任何 `Shape` 類型的值都能被正常使用。
你也可以將不透明返回類型和泛型結合起來,下面的兩個泛型函數也都返回了遵循 `Shape` 協議的不透明類型。
```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` 的值和前文 [不透明類型解決的問題](#the-problem-that-opaque-types-solve) 中關于泛型的那個例子中的 `joinedTriangles` 完全一樣。不過和前文不一樣的是,`flip(-:)` 和 `join(-:-:)` 將對泛型參數的操作后的返回結果包裝成了不透明類型,這樣保證了在結果中泛型參數類型不可見。兩個函數都是泛型函數,因為他們都依賴于泛型參數,而泛型參數又將 `FlippedShape` 和 `JoinedShape` 所需要的類型信息傳遞給它們。
如果函數中有多個地方返回了不透明類型,那么所有可能的返回值都必須是同一類型。即使對于泛型函數,不透明返回類型可以使用泛型參數,但仍需保證返回類型唯一。比如,下面就是一個*非法*示例 —— 包含針對 `Square` 類型進行特殊處理的翻轉函數。
```swift
func invalidFlip<T: Shape>(_ shape: T) -> some Shape {
if shape is Square {
return shape // 錯誤:返回類型不一致
}
return FlippedShape(shape: shape) // 錯誤:返回類型不一致
}
```
如果你調用這個函數時傳入一個 `Square` 類型,那么它會返回 `Square` 類型;否則,它會返回一個 `FlippedShape` 類型。這違反了返回值類型唯一的要求,所以 `invalidFlip(_:)` 不正確。修正 `invalidFlip(_:)` 的方法之一就是將針對 `Square` 的特殊處理移入到 `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]`, 所以這符合不透明返回類型始終唯一的要求。
## 不透明類型和協議類型的區別 {#differences-between-opaque-types-and-protocol-types}
雖然使用不透明類型作為函數返回值,看起來和返回協議類型非常相似,但這兩者有一個主要區別,就在于是否需要保證類型一致性。一個不透明類型只能對應一個具體的類型,即便函數調用者并不能知道是哪一種類型;協議類型可以同時對應多個類型,只要它們都遵循同一協議。總的來說,協議類型更具靈活性,底層類型可以存儲更多樣的值,而不透明類型對這些底層類型有更強的限定。
比如,這是 `flip(_:)` 方法不采用不透明類型,而采用返回協議類型的版本:
```swift
func protoFlip<T: Shape>(_ shape: T) -> Shape {
return FlippedShape(shape: shape)
}
```
這個版本的 `protoFlip(_:)` 和 `flip(_:)` 有相同的函數體,并且它也始終返回唯一類型。但不同于 `flip(_:)`,`protoFlip(_:)` 返回值其實不需要始終返回唯一類型 —— 返回類型只需要遵循 `Shape` 協議即可。換句話說,`protoFlip(_:)` 比起 `flip(_:)` 對 API 調用者的約束更加松散。它保留了返回多種不同類型的靈活性:
```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 // 錯誤
```
上面的例子中,最后一行的錯誤來源于多個原因。最直接的問題在于,`Shape` 協議中并沒有包含對 == 運算符的聲明。如果你嘗試加上這個聲明,那么你會遇到新的問題,就是 == 運算符需要知道左右兩側參數的類型。這類運算符通常會使用 `Self` 類型作為參數,用來匹配符合協議的具體類型,但是由于將協議當成類型使用時會發生類型擦除,所以并不能給協議加上對 `Self` 的實現要求。
將協議類型作為函數的返回類型能更加靈活,函數只要返回遵循協議的類型即可。然而,更具靈活性導致犧牲了對返回值執行某些操作的能力。上面的例子就說明了為什么不能使用 == 運算符 —— 它依賴于具體的類型信息,而這正是使用協議類型所無法提供的。
這種方法的另一個問題在于,變換形狀的操作不能嵌套。翻轉三角形的結果是一個 `Shape` 類型的值,而 `protoFlip(_:)` 方法的則將遵循 `Shape` 協議的類型作為形參,然而協議類型的值并不遵循這個協議;`protoFlip(_:)` 的返回值也并不遵循 `Shape` 協議。這就是說 `protoFlip(protoFlip(smallTriange))` 這樣的多重變換操作是非法的,因為經過翻轉操作后的結果類型并不能作為 `protoFlip(_:)` 的形參。
相比之下,不透明類型則保留了底層類型的唯一性。Swift 能夠推斷出關聯類型,這個特點使得作為函數返回值,不透明類型比協議類型有更大的使用場景。比如,下面這個例子是 [泛型](./22_Generics.md) 中講到的 `Container` 協議:
```swift
protocol Container {
associatedtype Item
var count: Int { get }
subscript(i: Int) -> Item { get }
}
extension Array: Container { }
```
你不能將 `Container` 作為方法的返回類型,因為此協議有一個關聯類型。你也不能將它用于對泛型返回類型的約束,因為函數體之外并沒有暴露足夠多的信息來推斷泛型類型。
```swift
// 錯誤:有關聯類型的協議不能作為返回類型。
func makeProtocolContainer<T>(item: T) -> Container {
return [item]
}
// 錯誤:沒有足夠多的信息來推斷 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))
// 輸出 "Int"
```
`twelve` 的類型可以被推斷出為 `Int`, 這說明了類型推斷適用于不透明類型。在 `makeOpaqueContainer(item:)` 的實現中,底層類型是不透明集合 `[T]`。在上述這種情況下,`T` 就是 `Int` 類型,所以返回值就是整數數組,而關聯類型 `Item` 也被推斷出為 `Int`。`Container` 協議中的 `subscipt` 方法會返回 `Item`,這也意味著 `twelve` 的類型也被能推斷出為 `Int`。
- 1.關于 Swift
- 2.Swift 初見
- 2-1基礎部分
- 2-2基本運算符
- 2-3字符串和字符
- 2-4集合類型
- 2-5控制流
- 2-6函數
- 2-7閉包
- 2-8枚舉
- 2-9類和結構體
- 2-10屬性
- 2-11方法
- 2-12下標
- 2-13繼承
- 2-14構造過程
- 2-15析構過程
- 2-16可選鏈
- 2-17錯誤處理
- 2-18類型轉換
- 2-19嵌套類型
- 2-20擴展
- 2-21協議
- 2-22泛型
- 2-23不透明類型
- 2-24自動引用計數
- 2-25內存安全
- 2-26訪問控制
- 2-27高級運算符
- 3-1關于語言參考
- 3-2詞法結構
- 3-3類型
- 3-4表達式
- 3-5語句
- 3-6聲明
- 3-7特性
- 3-8模式
- 3-9泛型參數
- 4語法總結