這里的教程為Swift官方教程中文版。

不透明類型

具有不透明返回類型的函數(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)。諸如 JoinedShapeFlippedShape 這樣包裝后的類型,模塊使用者并不關(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 { ... }

xy 的值由調(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ù)又將 FlippedShapeJoinedShape 所需要的類型信息傳遞給它們。

如果函數(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], 所以這符合不透明返回類型始終唯一的要求。

不透明類型和協(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)說,協(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。

? 泛型 自動(dòng)引用計(jì)數(shù) ?
?