泛型語法改進第一彈 —— Opaque Result Types

四娘發表於2019-05-05

SE-0244 Opaque Result Types 提案前一段時間通過了 review 並且在 Swift 5.1 裡完成了實現,我最早閱讀這份提案的時候理解不是很透徹,今天比較仔細地讀了這篇 Improving the UI of generics 之後有了更多的認識,而且發現自己之前發的 tweet 裡有一些錯誤的認知,所以這裡寫篇文章,希望用最直白的方式解釋清楚提案的內容,跟大家分享一下我自己的理解。

Opaque Result Types?

用最最簡單的一句話來介紹這個提案的內容,就是它能讓被呼叫者選擇泛型返回值的具體型別。

這是什麼意思呢?讓我們來看目前最常見的泛型函式宣告:

protocol Shape { ... }

func generic<T: Shape>() -> T { ... }

let x: Rectangle = generic() // type(of: x) == Rectangle, 呼叫方決定返回值型別
複製程式碼

這很好,但有時候我們不希望暴露出具體的返回型別,也不想讓呼叫者去依賴具體的型別,之前我們可以直接使用泛型型別:

func generic() -> Shape { ... }

let x = generic() // type(of: x) == Shape
複製程式碼

雖然這樣確實能達成我們的目的,但也會帶來一些副作用,例如說效能問題,因為實際上 generic 返回的是一個例項的容器。

我們更希望的是能夠像第一種宣告那樣,在編譯時就確定返回值的具體型別,並且由被呼叫方去決定:

func reverseGeneric() -> some Shape { return Rectangle(...)  }

let x = reverseGeneric() 
// type(of: x) == Rectangle
// 並且 x 的型別根據 reverseGeneric 的具體實現決定
複製程式碼

通過引入 some 這個關鍵字去修飾返回值,就可以讓被呼叫方選擇具體的返回值型別,並且是在編譯時確定下來的,這意味著我們不需要額外的容器去存放返回的實際值。

另外它還可以作為屬性使用:

func someNumber() -> some Numeric { ... }

var number: some Numeric = someNumber()
複製程式碼

它可以?

在這裡我們需要先確立一個條件,通過 some 修飾的型別,都會在編譯時確定下來,可以簡單地理解為被呼叫方負責傳入的一個泛型引數,相關的功能和限制都是基於這個特性延伸出來的。

根據前面的條件,如果返回值使用了 some 修飾,編譯器可以推匯出兩件很重要的事情:

  1. 同一個函式簽名,返回值的型別肯定也只能是同一個具體型別
  2. 外部不能夠依賴函式實現裡使用的返回值具體型別

這意味著什麼?在之前,兩個遵循了 Equatable 的例項不能判斷是否相等,因為我們並不知道它們具體的型別是否一樣,但如果使用了 some,並且是由同一個函式返回的,那就完全不是問題了:

func randomNumber() -> some Equatable {
    let i: Int = 32
    return i
}

let x = randomNumber()
let y = randomNumber()

// 在這裡使用 == 是沒問題的
// 因為 x 跟 y 都是由 randomNumber 返回的
// 所以它們的具體型別必然一致
print(x == y)
複製程式碼

但是控制返回值型別的是呼叫方,所以你不能夠依賴呼叫方的具體實現:

func randomNumber() -> some Equatable {
    let i: Int = 32
    return i
}

var otherNumber: Int = 38
var x = randomNumber()
x = otherNumber // error
複製程式碼

雖然你知道 randomEquatableNumber 的實現裡使用的是 Int,但具體的實現有可能隨時被調整,所以你不能依賴它。

結語

在這裡只是簡單地介紹了一些比較核心的內容,想了解更多細節的朋友可以看提案。實際上這個提案只是整個泛型語法改進計劃裡的第一步,之後等我有了更加深入的瞭解再做更多分享。

覺得文章還不錯的話可以關注一下我的部落格

相關文章