Swift學習筆記 (四十二) 不透明類型

具有不透明返回類型的函數或方法會隱藏返回值的類型信息。函數不再提供具體的類型作爲返回類型,⽽是根據它支持的協議來

描述返回值。在處理模塊和調用代碼之間的關係時,隱藏類型信息⾮常有用,因爲返回的底層數據類型仍然可以保持私有。而且

不同於返回協議類型,不透明類型能保證類型一致性 —— 編譯器能獲取到類型信息,同時模塊使用者卻不能獲取到。

 

不透明類型解決的問題

舉個例子,假設你正在寫一個模塊,用來繪製 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 。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章