Swift-不透明類型

  • 具有不透明返回類型的函數(shù)或方法會(huì)隱藏返回值的類型信息晾腔。
  • 函數(shù)不再提供具體的類型作為返回類型栓票,而是根據(jù)它支持的協(xié)議來(lái)描述返回值舞丛。
  • 在處理模塊和調(diào)用代碼之間的關(guān)系時(shí)构捡,隱藏類型信息非常有用独郎,因?yàn)榉祷氐牡讓訑?shù)據(jù)類型仍然可以保持私有踩麦。
  • 而且不同于返回協(xié)議類型,不透明類型能保證類型一致性 —— 編譯器能獲取到類型信息氓癌,同時(shí)模塊使用者卻不能獲取到谓谦。

1. 不透明類型解決的問(wèn)題

  • 假設(shè)你正在寫(xiě)一個(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 幾何圖形模塊的部分公開(kāi)接口必須聲明完整的返回類型巩螃,而實(shí)際上這些類型信息并不應(yīng)該被公開(kāi)聲明演怎。
輸出同一種幾何圖形,模塊內(nèi)部可能有多種實(shí)現(xiàn)方式避乏,而外部使用時(shí)爷耀,應(yīng)該與內(nèi)部各種變換順序的實(shí)現(xiàn)邏輯無(wú)關(guān)。
諸如 JoinedShape 和 FlippedShape 這樣包裝后的類型拍皮,模塊使用者并不關(guān)心歹叮,它們也不應(yīng)該可見(jiàn)跑杭。
模塊的公開(kāi)接口應(yīng)該由拼接、翻轉(zhuǎn)等基礎(chǔ)操作組成咆耿,這些操作也應(yīng)該返回獨(dú)立的 Shape 類型的值德谅。

2. 返回不透明類型

  • 可以認(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)寫(xiě)代碼吻商,才能應(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è)梯形,卻沒(méi)直接輸出梯形的底層類型:
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é)議的給定類型阳藻,而不需指定任何具體類型晰奖。
這樣寫(xiě) makeTrapezoid() 函數(shù)可以表明它公共接口的基本性質(zhì) —— 返回的是一個(gè)幾何圖形 —— 而不是部分的公共接口生成的特殊類型。
上述實(shí)現(xiàn)過(guò)程中使用了兩個(gè)三角形和一個(gè)正方形腥泥,還可以用其他多種方式重寫(xiě)畫(huà)梯形的函數(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 的值和前文 不透明類型解決的問(wèn)題 中關(guān)于泛型的那個(gè)例子中的 joinedTriangles 完全一樣。
不過(guò)和前文不一樣的是孩等,flip( _ : ) 和 join( _ : _ : ) 將對(duì)泛型參數(shù)的操作后的返回結(jié)果包裝成了不透明類型艾君,這樣保證了在結(jié)果中泛型參數(shù)類型不可見(jiàn)。
兩個(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]驯杜, 所以這符合不透明返回類型始終唯一的要求。

3. 不透明類型和協(xié)議類型的區(qū)別

  • 雖然使用不透明類型作為函數(shù)返回值做个,看起來(lái)和返回協(xié)議類型非常相似艇肴,但這兩者有一個(gè)主要區(qū)別,就在于是否需要保證類型一致性叁温。
  • 一個(gè)不透明類型只能對(duì)應(yīng)一個(gè)具體的類型再悼,即便函數(shù)調(diào)用者并不能知道是哪一種類型;協(xié)議類型可以同時(shí)對(duì)應(yīng)多個(gè)類型膝但,只要它們都遵循同一協(xié)議冲九。
  • 總的來(lái)說(shuō),協(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é)議即可仗岖。

  • 換句話說(shuō),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è)原因。
最直接的問(wèn)題在于遭铺,Shape 協(xié)議中并沒(méi)有包含對(duì) = = 運(yùn)算符的聲明丽柿。
如果你嘗試加上這個(gè)聲明,那么你會(huì)遇到新的問(wèn)題魂挂,就是 = = 運(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)致?tīng)奚藢?duì)返回值執(zhí)行某些操作的能力秋泳。
上面的例子就說(shuō)明了為什么不能使用 = = 運(yùn)算符 —— 它依賴于具體的類型信息潦闲,而這正是使用協(xié)議類型所無(wú)法提供的。

這種方法的另一個(gè)問(wèn)題在于迫皱,變換形狀的操作不能嵌套歉闰。
翻轉(zhuǎn)三角形的結(jié)果是一個(gè) Shape 類型的值,而 protoFlip( _ : ) 方法的則將遵循 Shape 協(xié)議的類型作為形參,然而協(xié)議類型的值并不遵循這個(gè)協(xié)議和敬;protoFlip( _ : ) 的返回值也并不遵循 Shape 協(xié)議凹炸。
這就是說(shuō) protoFlip(protoFlip(smallTriange)) 這樣的多重變換操作是非法的,因?yàn)榻?jīng)過(guò)翻轉(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ù)體之外并沒(méi)有暴露足夠多的信息來(lái)推斷泛型類型。

// 錯(cuò)誤:有關(guān)聯(lián)類型的協(xié)議不能作為返回類型蹬刷。
func makeProtocolContainer<T>(item: T) -> Container {
    return [item]
}
?
// 錯(cuò)誤:沒(méi)有足夠多的信息來(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泡态, 這說(shuō)明了類型推斷適用于不透明類型。
在 makeOpaqueContainer(item:) 的實(shí)現(xiàn)中迂卢,底層類型是不透明集合 [T]某弦。
在上述這種情況下,T 就是 Int 類型而克,所以返回值就是整數(shù)數(shù)組靶壮,而關(guān)聯(lián)類型 Item 也被推斷出為 Int。
Container 協(xié)議中的 subscipt 方法會(huì)返回 Item员萍,這也意味著 twelve 的類型也被能推斷出為 Int腾降。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市碎绎,隨后出現(xiàn)的幾起案子螃壤,更是在濱河造成了極大的恐慌,老刑警劉巖筋帖,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奸晴,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡日麸,警方通過(guò)查閱死者的電腦和手機(jī)蚁滋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人辕录,你說(shuō)我怎么就攤上這事睦霎。” “怎么了走诞?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵副女,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我蚣旱,道長(zhǎng)碑幅,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任塞绿,我火速辦了婚禮沟涨,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘异吻。我一直安慰自己裹赴,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布诀浪。 她就那樣靜靜地躺著棋返,像睡著了一般。 火紅的嫁衣襯著肌膚如雪雷猪。 梳的紋絲不亂的頭發(fā)上睛竣,一...
    開(kāi)封第一講書(shū)人閱讀 51,365評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音求摇,去河邊找鬼射沟。 笑死,一個(gè)胖子當(dāng)著我的面吹牛与境,可吹牛的內(nèi)容都是我干的验夯。 我是一名探鬼主播,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼嚷辅,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼簿姨!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起簸搞,我...
    開(kāi)封第一講書(shū)人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤扁位,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后趁俊,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體域仇,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年寺擂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了暇务。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泼掠。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖垦细,靈堂內(nèi)的尸體忽然破棺而出择镇,到底是詐尸還是另有隱情,我是刑警寧澤括改,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布腻豌,位于F島的核電站,受9級(jí)特大地震影響嘱能,放射性物質(zhì)發(fā)生泄漏吝梅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一惹骂、第九天 我趴在偏房一處隱蔽的房頂上張望苏携。 院中可真熱鬧,春花似錦对粪、人聲如沸右冻。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)国旷。三九已至矛物,卻和暖如春茫死,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背履羞。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工峦萎, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人忆首。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓爱榔,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親糙及。 傳聞我的和親對(duì)象是個(gè)殘疾皇子详幽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容