Swift 5.7 中的 any 和 some (譯)

Sunxb發表於2022-06-27

由於 anysome 都適用於協議,因此我想在這篇博文中將它們放在一起比較以便更好地解釋它們解決分別解決了什麼問題,以及在什麼情況下使用 anysome 或其他的。

瞭解 any 和 some 解決的問題

為了解釋 any 解決的問題,我們可以通過一個列子來了解這兩個關鍵字。下面是一個Pizza模型的協議:

protocol Pizza {
    var size: Int { get }
    var name: String { get }
}

在Swift 5.6,你可能會寫下面的這種方法,來接收一個Pizza

func receivePizza(_ pizza: Pizza) {
    print("Omnomnom, that's a nice \(pizza.name)")
}

當這個函式被呼叫時,receivePizza 函式接收一個所謂的披薩協議型別,我們可以理解為一個披薩盒子。為了知道這個披薩名稱,必須開啟這個盒子,也就是獲取實現Pizza協議的具體物件,然後獲取名稱。這意味著 Pizza 幾乎沒有編譯時優化,這使得 receivePizza 方法呼叫的開銷比我們想要的更大。

另外下面的函式,看起來好像是一樣的

func receivePizza<T: Pizza>(_ pizza: T) {
    print("Omnomnom, that's a nice \(pizza.name)")
}

不過,這裡有一個很主要區別。 Pizza 協議在這裡沒有用作引數型別。它被用作泛型 T 的約束。這就使得編譯器將能夠在編譯時解析 T 的型別,使得 receivePizza 接受到的是一個具體化的型別。

因為上面這兩種方法差異並不是很清楚,所以 Swift 團隊引入了 any 關鍵字。此關鍵字不新增任何新功能。它迫使我們清楚地傳達“這是一種存在主義”:(有點拗口,也不是很好理解,我就把他理解成這麼型別的一個東西)

// 上面的第一種寫法,增加一個any關鍵字
func receivePizza(_ pizza: any Pizza) {
    print("Omnomnom, that's a nice \(pizza.name)")
}

使用泛型T的示例不需要 any 關鍵字,因為 Pizza 被用作約束而不是存在型別。

現在我們對 any 有了更清晰的瞭解,繼續讓我們研究一下 some

在 Swift 中,許多開發人員都寫如下程式碼:

let someCollection: Collection

我們會遇到編譯器錯誤,告訴我們 Collection 有 Self 或關聯的型別要求。在 Swift 5.1 中,我們可以告訴編譯器任何訪問 someCollection 的人都不應該關心這些。他們應該只需要知道這個東西符合 Collection 協議 ,僅此而已。

這種機制對於使 SwiftUI 的 View 協議至關重要。

但也有缺點,那就是在使用some Colelction的時候,無法知道其中的關聯型別是什麼。

然而,並非所有協議都有相關的關聯型別。再次考慮下面這個receivePizza版本:

func receivePizza<T: Pizza>(_ pizza: T) {
    print("Omnomnom, that's a nice \(pizza.name)")
}

我們定義了一個通用的 T 來允許編譯器針對給定的具體型別的 Pizza 進行優化。 some 關鍵字還允許編譯器在編譯時知道 some 物件的底層實際型別是什麼;它只是對我們隱藏。這正是 <T: Pizza> 所做的。我們通過 T 這個型別訪問也只能訪問到 Pizza 協議所公開的內容。這意味著我們可以重寫 receivePizza<T: Pizza>(_:) 如下:

func receivePizza(_ pizza: some Pizza) {
    print("Omnomnom, that's a nice \(pizza.name)")
}

我們不再需要 T ,也就是我們不需要建立一個型別來代表我們的 Pizza。我們可以說“這個函式需要some Pizza”而不是“這個函式需要一些我們稱之為 T 的Pizza”。這兩種寫法是等價的。

選擇 some 還是 any

其實當我們瞭解了some 和 any, 就會知道這不選其中一個的問題,他們都解決自己的問題的。

一般來說,我們要儘可能的使用 some 或者泛型,就拿我們的 Pizza 來說,如果使用any, 就好比我們在執行時也會是接收到一個Pizza型別的盒子,具體是什麼Pizza, 還需要我們再開啟盒子,但是some 或者泛型,就會給我們一個實際的Pizza型別了。

實踐

讓我們再舉一個例子來說明這一點,這個例子在很大程度上借鑑了我對主要關聯型別的解釋。

class MusicPlayer {
    var playlist: any Collection<String> = []

    func play(_ playlist: some Collection<String>) {
        self.playlist = playlist
    }
}

在這段程式碼中,我使用了 some Collection<String> 而不是編寫 func play<T: Collection<String>>(_ playlist: T) ,因為泛型只在這一個地方使用。

我的 var playlistany Collection<String> 而不是 some Collection<String> 有兩個原因:

  1. 無法確保編譯器將為play方法推導的具體Colection與為var playlist推導的具體Colection相匹配;這意味著它們可能不一樣。
  2. 編譯器首先無法推斷var playlist:some Collection<String>(嘗試一下,你會得到一個編譯器錯誤)

我們可以用下面的寫法來避免使用 any :

class MusicPlayer<T: Collection<String>> {
    var playlist: T = []

    func play(_ playlist: T) {
        self.playlist = playlist
    }
}

但是這樣就會強制我們的 T 為同一型別,比如說我們在使用時 T 是Array, 那我們在play方法中就不能在傳入其他的Collection型別,比如說Set。但是前面的那種寫法是可以的。

總結

雖然 some 和 any 聽起來很複雜(事實上確實如此),但它們也是 Swift 5.7 中非常強大和重要的部分。理解他們是很有必要的,因為這可以幫助我們更好地理解 Swift 如何處理泛型和協議。

相關文章