Swift 程式語言 — Opaque Types

讓我們一起來看看什麼是 Opaque Types 吧!

Jeremy Xue
Jeremy Xue ‘s Blog
10 min readFeb 1, 2020

--

Photo by Siarhei Plashchynski on Unsplash

前言:

帶有不透明(opaque)返回類型的函數或方法,將會隱藏其返回值的類型一訓。取代提供實際類型作為函數的返回類型,返回值根據支援的協議進行描述。隱藏類型資訊在模組和調用該模組程式碼之間的邊界處很有用,因為返回值底層的類型可以保持私有。不同於返回類型為協議類型的值,不透明類型(Opaque Types)保留其類型身份 — 編譯器可以訪問類型資訊,但是模組的用戶端無法。

|Opaque Types 解決的問題

例如,假設你正編寫一個繪製 ASCll 形狀的模組。ASCll 形狀的基本特性是 draw() 函數,該函數返回字串形式表現該形狀,你可以將其作為 Shape 協議的要求:

你可以使用泛型來實現類似垂直翻轉形狀的操作,如同下面程式碼所示。但是,這種方式有一個重要的局限性:翻轉的結果會暴露用於創建它的確切泛型類型。

這種定義 JoinedShape<T: Shape, U: Shape> 結構的方法將兩個形狀垂直的連在一起,如下面的程式碼所示,一個翻轉了的三角形與另一個三角形結合後返回的結果類型類似 JoinedShape<FlippedShape<Triangle>, Triangle>

暴露有關形狀創建的詳細資訊,可以使原本不希望成為 ASCll 模組公開接口的一部分的類型洩漏出去,因為它們需要宣告完整的返回類型。模組內的程式碼可以透過多種方式建構相同的形狀,而使用該形狀的模組外的其他程式碼則不必考慮有關轉換列表的實現細節。包裝類型(Wrapper types)像是 JoinedShapeFlippedShape 對於模組的用戶而言無關緊要,它們也不應該顯示。該模組的公開接口包括連結和翻轉形狀之類的操作,這些操作會返回另一個 Shape 值。

|返回 Opaque Type

你可以將不透明類型想像成與泛型類型相反的類型。泛型類型使調用函數的程式碼可以從函數實現中抽象出來的方式來為該函數的參數選擇類型並返回值。例如,下列程式碼中的函數返回依賴於其調用者的類型:

調用 max(_:_:) 的程式碼選擇 xy 的值,這些值的類型決定了 T 的具體類型。調用程式碼可以符合 Comparable 協議的任何類型。函數內部的程式碼以泛型方式編寫,因此它可以處理調用者提供的任何類型。max(_:_:) 的實現僅使用所有 Comparable 類型共享的功能。

對於具有不透型返回類型的函數,這些腳色相反。不透明類型可以讓函數實現從返回調用函數的程式碼抽象出來的方式為返回的值選擇類型。例如,以下範例中的函數返回梯形而不暴露該形狀的底層類型。

在這個範例中,makeTrapezoid() 函數將其返回類型宣告為 some Shape;結果就是,該函數返回符合 Shape 協議的某些給定類型的值,而沒有指定任何特定的具體類型。以這種方式編寫 makeTrapezoid() 可以表達其公開接口中基本的期望(其返回的值是一個形狀),而不需指定形狀是由其公開接口的一部分製成的特定類型。此實現使用兩個三角形和一個正方形,但是可以重寫函數以多種其他方式繪製梯形而無需修改其返回類型。

這個範例強調了不透明返回類型類似於泛型類型的相反方式。makeTrapezoid() 中的程式碼可以返回所需的任何類型,只要該類型符合 Shape 協議,就如同調用程式碼對泛型函數所做的那樣。調用該函數的程式碼需要以泛型的方式編寫,例如泛型函數的實現,以便他可以與 makeTrapezoid() 返回的任何 Shape 值一起使用。

你還可以將不透明的返回類型與泛型結合使用。以下程式碼中的函數均返回符合 Shape 協議的某種類型的值。

這個範例中的 opaqueJoinedTriangles 的值與前面 “Opaque Types 解決的問題” 中的泛型範例中的 joinTriangles 相同。但是,與該範例中的值不同, flip(_:) join(_: _:) 將泛型形狀操作返回的底層類型包裝為不透明的返回類型,來防止這些類型的可見性。這兩個函數都是泛型的,因為他們所依賴的類型是泛型的,並且函數的類型參數傳遞了 FlippedShapeJoinedShape 所需的類型資訊。

如果返回不透明的函數從都個地方返回,則所有可能的返回值都必須具有相同的類型。對於泛型函數,該返回類型可以使用函數的泛型類型參數,但其仍必須是單個類型。例如,這是一個無效的形狀翻轉函數版本,其中包含正方形的特殊狀況:

如果使用 Square 調用此函數,它將返回 Square;否則,它將返回 FlippedShap。這違背了僅返回一種類型值的要求,並且使 invalidFlip(_:) 為無效的程式碼。修復 invalidFlip(_:) 的一種方法勢將特殊的正方形移動到 FlippedShape 的實現中,這使該函數始終返回 FlippedShape 值:

始終返回單個類型的要求並不阻止你在不透明的返回類型中使用泛型。這是一個函數的範例,該函數將其類型參數合併到其返回值的底層類型中:

在這種狀況下,返回值的底層類型依賴於 T:無論傳遞什麼形狀,repeat(shape:count:) 都會創建並返回該形狀的數組。儘管如此,返回值始終具有相同的 [T] 底層類型,因此它遵循具有不透明返回類型的函數必須僅返回單個類型值的要求。

|Opaque Types 與 Protocol Types 的差別

返回不透明類型看起來與使用協議類型(protocol type)作為函數的返回值非常相似,但是這兩種返回類型在是否保留類型身份方面有所不同。不透明類型引用特定的類型,儘管函數的調用者看不到是哪種類型;協議類型可以引用遵循協議的任何類型。一般來說,協議類型為你提供他們存儲值的底層類型更多的靈活性,不透明類型使你可以對這些底層類型做出更強而有力的保證。

舉個例子,這邊是一個 flip(_:) 版本,該版本返回協議類型的值,而不是使用不透明的返回類型:

此版本的 protoFlip(_:) 具有與 flip(_:) 相同的主體,並且總是返回相同類型的值。不同於 flip(_:)protoFlip(_:) 返回的值不必總是具有相同的類型,只需要遵循 Shape 協議即可。換句話說,protoFlip(_:) 與調用者之間的 API 約束要比 flip(_:) 更寬鬆,它保留返回多種類型值的靈活性:

修改後的程式碼版本返回 Square 實例或 FlippedShape 實例,依賴於傳入的形狀。此函數返回的兩個翻轉形狀可能具有兩個完全不同的實例。當翻轉相同形狀的多個實例時,此函數的其他有效版本可能返回不同類型的值。protoFlip(_: ) 具有更少的特定返回類型資訊,這意味著許多依賴於類型資訊的操作在返回值上無法使用。例如,無法編寫 == 運算符來比較此函數返回的結果。

出現在範例最後一行的錯誤有許多原因。而直接的問題是 Shape 不包含 == 運算符作為其協議要求的一部分。如果你嘗試添加,那麼你將遇到的下個問題是 == 運算符需要知道左手參數和右手參數的類型。這系列的運算符通常會採用類型為 Self 的參數,匹配遵循改協議的任何類型,但是在協議中添加 Self 要求並不能消除將協議作為類型時發生的類型擦除(type erasure)。

使用協議類型作為函數的返回類型,可以靈活的返回符合協議的任何類型。但是,這種靈活性的代價是這種靈活性的代價是無法對返回值進行某些操作。此範例展示了 == 運算符為什麼無法使用,它依賴於使用協議類型不能保留的特定類型資訊。

這種方法的另一個問題是形狀轉換不能嵌套。翻轉三角形的結果是 Shape 類型的值,並且 protoFlip(_:) 函數採用一個遵循 Shape 協議的某種類型的參數。然而,協議類型的值不遵循該協議;protoFlip(_:) 返回值不遵循 Shape。這意味著像 protoFlip(protoFlip(smallTriange)) 這樣的程式碼會進行多次轉換,因此無效,因為翻轉後的形狀不是 protoFlip(_:) 的有效參數。

相反的,不透明類型保留了底層類型的標誌。Swift 可以推斷關聯類型(associated type),這使你可以在協議類型不能用作返回值的地方使用不透明的返回值。例如,這是泛型中提到的 Container 協議版本:

你不能使用 Cotainer 作為函數的返回類型,因為具有關聯類型。你也不能將其作為泛型返回類型的約束,因為函數主體外沒有足夠的資訊來推斷泛型函數需要是什麼。

使用不透明類型將 some Container 作為返回類型表示所需的 API 約束 — 該函數返回一個容器,但拒絕指定該容器類型:

twelve 類型推斷為 Int,這說明了類型推斷適用於不透明類型的事實。在 makeOpaqueContainer(item:) 實現中,不透明容器的底層類型為 [T]。在這種情況下,TInt,因此返回值是一個整數數組,並且 Item 被關聯類型推斷為 IntContainer 的下標返回 Item,這意味著 twelve 的類型也被推斷為 Int

--

--

Jeremy Xue
Jeremy Xue ‘s Blog

Hi, I’m Jeremy. [好想工作室 — iOS Developer]