具有不透明返回類型的函數(shù)或方法會(huì)隱藏返回值的類型信息。函數(shù)不再提供具體的類型作為返回類型,而是根據(jù)它支持的協(xié)議來(lái)描述返回值。在處理模塊和調(diào)用代碼之間的關(guān)系時(shí),隱藏類型信息非常有用,因?yàn)榉祷氐牡讓訑?shù)據(jù)類型仍然可以保持私有。而且不同于返回協(xié)議類型,不透明類型能保證類型一致性 —— 編譯器能獲取到類型信息,同時(shí)模塊使用者卻不能獲取到。
舉個(gè)例子,假設(shè)你正在寫一個(gè)模塊,用來(lái)繪制 ASCII 符號(hào)構(gòu)成的幾何圖形。它的基本特征是有一個(gè) draw()
方法,會(huì)返回一個(gè)代表最終幾何圖形的字符串,你可以用包含這個(gè)方法的 Shape
協(xié)議來(lái)描述:
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())
// *
// **
// ***
你可以利用泛型來(lái)實(shí)現(xiàn)垂直翻轉(zhuǎn)之類的操作,就像下面這樣。然而,這種方式有一個(gè)很大的局限:翻轉(zhuǎn)操作的結(jié)果會(huì)暴露我們用于構(gòu)造結(jié)果的泛型類型:
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())
// ***
// **
// *
如下方代碼所示,用同樣的方式定義了一個(gè) JoinedShape<T: Shape, U: Shape>
結(jié)構(gòu)體,能將幾何圖形垂直拼接起來(lái)。如果拼接一個(gè)翻轉(zhuǎn)三角形和一個(gè)普通三角形,它就會(huì)得到類似于 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())
// *
// **
// ***
// ***
// **
// *
暴露構(gòu)造所用的具體類型會(huì)造成類型信息的泄露,因?yàn)?ASCII 幾何圖形模塊的部分公開接口必須聲明完整的返回類型,而實(shí)際上這些類型信息并不應(yīng)該被公開聲明。輸出同一種幾何圖形,模塊內(nèi)部可能有多種實(shí)現(xiàn)方式,而外部使用時(shí),應(yīng)該與內(nèi)部各種變換順序的實(shí)現(xiàn)邏輯無(wú)關(guān)。諸如 JoinedShape
和 FlippedShape
這樣包裝后的類型,模塊使用者并不關(guān)心,它們也不應(yīng)該可見。模塊的公開接口應(yīng)該由拼接、翻轉(zhuǎn)等基礎(chǔ)操作組成,這些操作也應(yīng)該返回獨(dú)立的 Shape
類型的值。
你可以認(rèn)為不透明類型和泛型相反。泛型允許調(diào)用一個(gè)方法時(shí),為這個(gè)方法的形參和返回值指定一個(gè)與實(shí)現(xiàn)無(wú)關(guān)的類型。舉個(gè)例子,下面這個(gè)函數(shù)的返回值類型就由它的調(diào)用者決定:
func max<T>(_ x: T, _ y: T) -> T where T: Comparable { ... }
x
和 y
的值由調(diào)用 max(_:_:)
的代碼決定,而它們的類型決定了 T
的具體類型。調(diào)用代碼可以使用任何遵循了 Comparable
協(xié)議的類型,函數(shù)內(nèi)部也要以一種通用的方式來(lái)寫代碼,才能應(yīng)對(duì)調(diào)用者傳入的各種類型。max(_:_:)
的實(shí)現(xiàn)就只使用了所有遵循 Comparable
協(xié)議的類型共有的特性。
而在返回不透明類型的函數(shù)中,上述角色發(fā)生了互換。不透明類型允許函數(shù)實(shí)現(xiàn)時(shí),選擇一個(gè)與調(diào)用代碼無(wú)關(guān)的返回類型。比如,下面的例子返回了一個(gè)梯形,卻沒直接輸出梯形的底層類型:
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())
// *
// **
// **
// **
// **
// *
這個(gè)例子中,makeTrapezoid()
函數(shù)將返回值類型定義為 some Shape
;因此,該函數(shù)返回遵循 Shape
協(xié)議的給定類型,而不需指定任何具體類型。這樣寫 makeTrapezoid()
函數(shù)可以表明它公共接口的基本性質(zhì) —— 返回的是一個(gè)幾何圖形 —— 而不是部分的公共接口生成的特殊類型。上述實(shí)現(xiàn)過程中使用了兩個(gè)三角形和一個(gè)正方形,還可以用其他多種方式重寫畫梯形的函數(shù),都不必改變返回類型。
這個(gè)例子凸顯了不透明返回類型和泛型的相反之處。makeTrapezoid()
中代碼可以返回任意它需要的類型,只要這個(gè)類型是遵循 Shape
協(xié)議的,就像調(diào)用泛型函數(shù)時(shí)可以使用任何需要的類型一樣。這個(gè)函數(shù)的調(diào)用代碼需要采用通用的方式,就像泛型函數(shù)的實(shí)現(xiàn)代碼一樣,這樣才能讓 makeTrapezoid()
返回的任何 Shape
類型的值都能被正常使用。
你也可以將不透明返回類型和泛型結(jié)合起來(lái),下面的兩個(gè)泛型函數(shù)也都返回了遵循 Shape
協(xié)議的不透明類型。
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())
// *
// **
// ***
// ***
// **
// *
這個(gè)例子中 opaqueJoinedTriangles
的值和前文 不透明類型解決的問題 中關(guān)于泛型的那個(gè)例子中的 joinedTriangles
完全一樣。不過和前文不一樣的是,flip(-:)
和 join(-:-:)
將對(duì)泛型參數(shù)的操作后的返回結(jié)果包裝成了不透明類型,這樣保證了在結(jié)果中泛型參數(shù)類型不可見。兩個(gè)函數(shù)都是泛型函數(shù),因?yàn)樗麄兌家蕾囉诜盒蛥?shù),而泛型參數(shù)又將 FlippedShape
和 JoinedShape
所需要的類型信息傳遞給它們。
如果函數(shù)中有多個(gè)地方返回了不透明類型,那么所有可能的返回值都必須是同一類型。即使對(duì)于泛型函數(shù),不透明返回類型可以使用泛型參數(shù),但仍需保證返回類型唯一。比如,下面就是一個(gè)非法示例 —— 包含針對(duì) Square
類型進(jìn)行特殊處理的翻轉(zhuǎn)函數(shù)。
func invalidFlip<T: Shape>(_ shape: T) -> some Shape {
if shape is Square {
return shape // 錯(cuò)誤:返回類型不一致
}
return FlippedShape(shape: shape) // 錯(cuò)誤:返回類型不一致
}
如果你調(diào)用這個(gè)函數(shù)時(shí)傳入一個(gè) Square
類型,那么它會(huì)返回 Square
類型;否則,它會(huì)返回一個(gè) FlippedShape
類型。這違反了返回值類型唯一的要求,所以 invalidFlip(_:)
不正確。修正 invalidFlip(_:)
的方法之一就是將針對(duì) Square
的特殊處理移入到 FlippedShape
的實(shí)現(xiàn)中去,這樣就能保證這個(gè)函數(shù)始終返回 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")
}
}
返回類型始終唯一的要求,并不會(huì)影響在返回的不透明類型中使用泛型。比如下面的函數(shù),就是在返回的底層類型中使用了泛型參數(shù):
func `repeat`<T: Shape>(shape: T, count: Int) -> some Collection {
return Array<T>(repeating: shape, count: count)
}
這種情況下,返回的底層類型會(huì)根據(jù) T
的不同而發(fā)生變化:但無(wú)論什么形狀被傳入,repeat(shape:count:)
都會(huì)創(chuàng)建并返回一個(gè)元素為相應(yīng)形狀的數(shù)組。盡管如此,返回值始終還是同樣的底層類型 [T]
, 所以這符合不透明返回類型始終唯一的要求。
雖然使用不透明類型作為函數(shù)返回值,看起來(lái)和返回協(xié)議類型非常相似,但這兩者有一個(gè)主要區(qū)別,就在于是否需要保證類型一致性。一個(gè)不透明類型只能對(duì)應(yīng)一個(gè)具體的類型,即便函數(shù)調(diào)用者并不能知道是哪一種類型;協(xié)議類型可以同時(shí)對(duì)應(yīng)多個(gè)類型,只要它們都遵循同一協(xié)議。總的來(lái)說,協(xié)議類型更具靈活性,底層類型可以存儲(chǔ)更多樣的值,而不透明類型對(duì)這些底層類型有更強(qiáng)的限定。
比如,這是 flip(_:)
方法不采用不透明類型,而采用返回協(xié)議類型的版本:
func protoFlip<T: Shape>(_ shape: T) -> Shape {
return FlippedShape(shape: shape)
}
這個(gè)版本的 protoFlip(_:)
和 flip(_:)
有相同的函數(shù)體,并且它也始終返回唯一類型。但不同于 flip(_:)
,protoFlip(_:)
返回值其實(shí)不需要始終返回唯一類型 —— 返回類型只需要遵循 Shape
協(xié)議即可。換句話說,protoFlip(_:)
比起 flip(_:)
對(duì) API 調(diào)用者的約束更加松散。它保留了返回多種不同類型的靈活性:
func protoFlip<T: Shape>(_ shape: T) -> Shape {
if shape is Square {
return shape
}
return FlippedShape(shape: shape)
}
修改后的代碼根據(jù)代表形狀的參數(shù)的不同,可能返回 Square
實(shí)例或者 FlippedShape
實(shí)例,所以同樣的函數(shù)可能返回完全不同的兩個(gè)類型。當(dāng)翻轉(zhuǎn)相同形狀的多個(gè)實(shí)例時(shí),此函數(shù)的其他有效版本也可能返回完全不同類型的結(jié)果。protoFlip(_:)
返回類型的不確定性,意味著很多依賴返回類型信息的操作也無(wú)法執(zhí)行了。舉個(gè)例子,這個(gè)函數(shù)的返回結(jié)果就不能用 == 運(yùn)算符進(jìn)行比較了。
let protoFlippedTriangle = protoFlip(smallTriangle)
let sameThing = protoFlip(smallTriangle)
protoFlippedTriangle == sameThing // 錯(cuò)誤
上面的例子中,最后一行的錯(cuò)誤來(lái)源于多個(gè)原因。最直接的問題在于,Shape
協(xié)議中并沒有包含對(duì) == 運(yùn)算符的聲明。如果你嘗試加上這個(gè)聲明,那么你會(huì)遇到新的問題,就是 == 運(yùn)算符需要知道左右兩側(cè)參數(shù)的類型。這類運(yùn)算符通常會(huì)使用 Self
類型作為參數(shù),用來(lái)匹配符合協(xié)議的具體類型,但是由于將協(xié)議當(dāng)成類型使用時(shí)會(huì)發(fā)生類型擦除,所以并不能給協(xié)議加上對(duì) Self
的實(shí)現(xiàn)要求。
將協(xié)議類型作為函數(shù)的返回類型能更加靈活,函數(shù)只要返回遵循協(xié)議的類型即可。然而,更具靈活性導(dǎo)致犧牲了對(duì)返回值執(zhí)行某些操作的能力。上面的例子就說明了為什么不能使用 == 運(yùn)算符 —— 它依賴于具體的類型信息,而這正是使用協(xié)議類型所無(wú)法提供的。
這種方法的另一個(gè)問題在于,變換形狀的操作不能嵌套。翻轉(zhuǎn)三角形的結(jié)果是一個(gè) Shape
類型的值,而 protoFlip(_:)
方法的則將遵循 Shape
協(xié)議的類型作為形參,然而協(xié)議類型的值并不遵循這個(gè)協(xié)議;protoFlip(_:)
的返回值也并不遵循 Shape
協(xié)議。這就是說 protoFlip(protoFlip(smallTriange))
這樣的多重變換操作是非法的,因?yàn)榻?jīng)過翻轉(zhuǎn)操作后的結(jié)果類型并不能作為 protoFlip(_:)
的形參。
相比之下,不透明類型則保留了底層類型的唯一性。Swift 能夠推斷出關(guān)聯(lián)類型,這個(gè)特點(diǎn)使得作為函數(shù)返回值,不透明類型比協(xié)議類型有更大的使用場(chǎng)景。比如,下面這個(gè)例子是 泛型 中講到的 Container
協(xié)議:
protocol Container {
associatedtype Item
var count: Int { get }
subscript(i: Int) -> Item { get }
}
extension Array: Container { }
你不能將 Container
作為方法的返回類型,因?yàn)榇藚f(xié)議有一個(gè)關(guān)聯(lián)類型。你也不能將它用于對(duì)泛型返回類型的約束,因?yàn)楹瘮?shù)體之外并沒有暴露足夠多的信息來(lái)推斷泛型類型。
// 錯(cuò)誤:有關(guān)聯(lián)類型的協(xié)議不能作為返回類型。
func makeProtocolContainer<T>(item: T) -> Container {
return [item]
}
// 錯(cuò)誤:沒有足夠多的信息來(lái)推斷 C 的類型。
func makeProtocolContainer<T, C: Container>(item: T) -> C {
return [item]
}
而使用不透明類型 some Container
作為返回類型,就能夠明確地表達(dá)所需要的 API 契約 —— 函數(shù)會(huì)返回一個(gè)集合類型,但并不指明它的具體類型:
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:)
的實(shí)現(xiàn)中,底層類型是不透明集合 [T]
。在上述這種情況下,T
就是 Int
類型,所以返回值就是整數(shù)數(shù)組,而關(guān)聯(lián)類型 Item
也被推斷出為 Int
。Container
協(xié)議中的 subscipt
方法會(huì)返回 Item
,這也意味著 twelve
的類型也被能推斷出為 Int
。