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