具有不透明返回類型的函數或方法會隱藏返回值的類型信息。函數不再提供具體的類型作爲返回類型,⽽是根據它支持的協議來
描述返回值。在處理模塊和調用代碼之間的關係時,隱藏類型信息⾮常有用,因爲返回的底層數據類型仍然可以保持私有。而且
不同於返回協議類型,不透明類型能保證類型一致性 —— 編譯器能獲取到類型信息,同時模塊使用者卻不能獲取到。
不透明類型解決的問題
舉個例子,假設你正在寫一個模塊,用來繪製 ASCII 符號構成的幾何圖形。它的基本特徵是有一個 draw() ⽅法,會返回一個代表
最終幾何圖形的字符串,你可以用包含這個方法的 Shape 協議來描述:
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())
// *
// **
// ***
你可以利⽤泛型來實現垂直翻轉之類的操作,就像下面這樣。然而,這種⽅式有一個很大的侷限:翻轉操作的結果會暴露我們用於
構造結果的泛型類型:
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> 這樣的類型。
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 類型的值。
返回不透明類型
你可以認爲不透明類型和泛型相反。泛型允許調用一個方法時,爲這個方法的形參和返回值指定一個與實現無關的類型。 舉個例
子,下面這個函數的返回值類型就由它的調用者決定:
func max<T>(_ x: T, _ y: T) -> T where T: Comparable { ... }
x 和 y 的值由調用 max(_:_:) 的代碼決定,而它們的類型決定了 T 的具體類型。調用代碼可以使用任何遵循了 Comparable 協議的
類型,函數內部也要以一種通用的方式來寫代碼,才能應對調用者傳入的各種類型。 max(_:_:)的實現就只使用了所有遵循
Comparable 協議的類型共有的特性。
⽽在返回不透明類型的函數中,上述⻆色發生了互換。不透明類型允許函數實現時,選擇一個與調用代碼無關的返回類型。比
如,下面的例子返回了一個梯形,卻沒直接輸出梯形的底層類型:
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 協議的不透明類型。
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 所需要的
類型信息傳遞給它們。
如果函數中有多個地方返回了不透明類型,那麼所有可能的返回值都必須是同一類型。即使對於泛型函數,不透明返回類型可以
使用泛型參數,但仍需保證返回類型唯一。⽐如,下⾯就是一個非法示例 —— 包含針對 Square 類型進⾏特殊處理的翻轉函數。
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 :
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")
}
}
返回類型始終唯一的要求,並不會影響在返回的不透明類型中使用泛型。比如下⾯的函數,就是在返回的底層類型中使用了泛型
參數:
func `repeat`<T: Shape>(shape: T, count: Int) -> some Collection {
return Array<T>(repeating: shape, count: count)
}
這種情況下,返回的底層類型會根據 T 的不同而發生變化:但無論什麼形狀被傳入, repeat(shape:count:) 都會創建並返回一個元
素爲相應形狀的數組。儘管如此,返回值始終還是同樣的底層類型 [T] , 所以這符合不透明返回類型始終唯一的要求。
不透明類型和協議類型的區別
雖然使用不透明類型作爲函數返回值,看起來和返回協議類型非常相似,但這兩者有一個主要區別,就在於是否需要保證類型⼀
致性。一個不透明類型只能對應一個具體的類型,即便函數調用者並不能知道是哪一種類型;協議類型可以同時對應多個類型,只
要它們都遵循同一協議。總的來說,協議類型更具靈活性,底層類型可以存儲更多樣的值,⽽不透明類型對這些底層類型有更強
的限定。
比如,這是 flip(_:) 方法不採用不透明類型,而採用返回協議類型的版本:
func protoFlip<T: Shape>(_ shape: T) -> Shape {
return FlippedShape(shape: shape)
}
這個版本的 protoFlip(_:) 和 flip(_:) 有相同的函數體,並且它也始終返回唯一類型。但不同於 flip(_:) , protoFlip(_:) 返回值其實不
需要始終返回唯一類型 —— 返回類型只需要遵循 Shape 協議即可。換句話說, protoFlip(_:) 比起 flip(_:) 對 API 調用者的約束更
加鬆散。它保留了返回多種不同類型的靈活性:
func protoFlip<T: Shape>(_ shape: T) -> Shape {
if shape is Square {
return shape
}
return FlippedShape(shape: shape)
}
修改後的代碼根據代表形狀的參數的不同,可能返回 Square 實例或者 FlippedShape 實例,所以同樣的函數可能返回完全不同的
兩個類型。當翻轉相同形狀的多個實例時,此函數的其他有效版本也可能返回完全不同類型的結果。 protoFlip(_:) 返回類型的不
確定性,意味着很多依賴返回類型信息的操作也無法執行了。舉個例子,這個函數的返回結果就不能用 == 運算符進⾏⽐較了。
let protoFlippedTriangle = protoFlip(smallTriangle)
let sameThing = protoFlip(smallTriangle)
protoFlippedTriangle == sameThing // 錯誤
上⾯的例子中,最後一⾏的錯誤來源於多個原因。最直接的問題在於, Shape 協議中並沒有包含對 == 運算符的聲明。 如果你嘗
試加上這個聲明,那麼你會遇到新的問題,就是 == 運算符需要知道左右兩側參數的類型。這類運算符通常會使用 Self 類型作爲
參數,用來匹配符合協議的具體類型,但是由於將協議當成類型使用時會發生類型擦除,所以並不能給協議加上對 Self 的實現要
求。
將協議類型作爲函數的返回類型能更加靈活,函數只要返回遵循協議的類型即可。然而,更具靈活性導致犧牲了對返回值執行某
些操作的能力。上面的例子就說明了爲什麼不能使用 == 運算符 —— 它依賴於具體的類型信息,而這正是使用協議類型所無法提
供的。
這種方法的另一個問題在於,變換形狀的操作不能嵌套。翻轉三⻆形的結果是一個 Shape 類型的值,而protoFlip(_:) 方法的則將
遵循 Shape 協議的類型作爲形參,然而協議類型的值並不遵循這個協議; protoFlip(_:) 的返回值也並不遵循 Shape 協議。這就是
說 protoFlip(protoFlip(smallTriange)) 這樣的多重變換操作是非法的,因爲經過翻轉操作後的結果類型並不能作爲 protoFlip(_:) 的
形參。
相⽐之下,不透明類型則保留了底層類型的唯一性。Swift 能夠推斷出關聯類型,這個特點使得作爲函數返回值,不透明類型比
協議類型有更大的使用場景。比如,下面這個例子是《泛型》中講到的 Container 協議:
protocol Container {
associatedtype Item
var count: Int { get }
subscript(i: Int) -> Item { get }
}
extension Array: Container { }
你不能將 Container 作爲方法的返回類型,因爲此協議有一個關聯類型。你也不能將它用於對泛型返回類型的約束, 因爲函數體
之外並沒有暴露足夠多的信息來推斷泛型類型。
// 錯誤:有關聯類型的協議不不能作爲返回類型。
func makeProtocolContainer<T>(item: T) -> Container {
return [item]
}
// 錯誤:沒有⾜足夠多的信息來推斷 C 的類型。
func makeProtocolContainer<T, C: Container>(item: T) -> C {
return [item]
}
⽽使用不透明類型 some Container 作爲返回類型,就能夠明確地表達所需要的 API 契約 —— 函數會返回一個集合類型,但並不
指明它的具體類型:
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 。