文章翻譯自 Avoiding near-duplicates in sets, 作者Paul Hudson @twostraws是一名優秀的Swifter。 這是我第一次翻譯,可能有翻譯不到位的地方,如果有任何問題,歡迎反饋。學習學習再學習,加油💪!
Julian Schiavo寫道:我想用Set集合來保證我的Array中元素是唯一的,但是Set集合中每個元素都包含一個Date類型的變量,當兩個不同元素僅僅是Date變量不同的時候,實際上Set中可以同時保存這兩個元素,這就出現了重復元素。這種問題該怎么解決呢?
這是個好問題,實際上Swift的協議給我們提供了很聰明的解決方案。
首先,我們先看下下面示例代碼。結構體NewsStory
有三個屬性:id、title、date:
struct NewsStory {
var id: Int
var title: String
var date = Date()
}
如上代碼所示,結構體實例初始化時候會自動將當前時間賦值給date屬性。
我們可以用上面的結構體創建三個對象,如下代碼所示:
let story1 = NewsStory(id: 1, title: "What's new in Swift 5.1?")
let story2 = NewsStory(id: 2, title: "What's new in Swift 6.0?")
let story3 = NewsStory(id: 3, title: "What's new in Swift 6.1?")
Julian想要保存這些新的對象到一個Set集合而不是數組中,這是一個很明智的選擇。因此我們寫下如下的代碼:
var stories = Set<NewsStory>()
stories.insert(story1)
stories.insert(story2)
stories.insert(story3)
print(stories)
如上代碼所示,創建一個保存故事對象的Set,然后將我們創建的對象添加到Set集合中,然后打印這個Set集合。然而上面的代碼無法通過編譯:為了每個元素在Set中都有唯一的標識,我們需要讓NewStory
對象遵守Hashable
協議,Hashable協議能夠產生唯一的hash值來標識唯一的一個對象。
Swift語言這點做得非常好,我們只需要讓一個包含Hashable屬性的類型遵守Hashable
協議即可,Hashable協議會自動幫我們計算這個對象的哈希值。因此我們需要更新NewStory
結構體如下:
struct NewsStory: Hashable {
var id: Int
var title: String
var date = Date()
}
到現在,我們的代碼終於能夠正常的跑起來啦!
然后,Julian遇到的問題並沒有解決,如下代碼所以:
let story4 = NewsStory(id: 1, title: "What's new in Swift 5.1?")
stories.insert(story4)
print(stories)
當我們創建一個和已存在對象相同ID和title的NewStory對象,並添加到set集合中,然后打印集合的內容,你會發現現在集合中包含4個對象,並且其中有一個是重復的。
就像前面寫的那樣,當一個類型遵守Hashable協議並且其屬性也都遵守Hashable協議的時候,Swift會幫我們自動計算這個對象的hash值。計算方法是這樣的:獲取對象中所有屬性的hash值並將它們結合在一起。
因此,我們以為兩個對象是相同的,因為他們有相同的ID和title,但是在Swift看來他們是不同的,因為他們的date並不相同。
我們需要做的就是給Swift提供一個自定義的hash計算規則,告訴Swift說"如果兩個stories對象的ID和title是相同的,那么他們就是相同的,請忽略date屬性。"
為了自定義hash計算規則,我們需要在NewStory
中實現兩個方法:一個是自定義計算hash值,兩一個是檢查兩個對象的唯一標識看是否相等。
第一個方法只使用ID來計算一個story對象的hash值,如下所示:
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
第二個方法使用運算符重載來實現一個自定義的==
方法來比較兩個story對象是否相同。
static func ==(lhs: NewsStory, rhs: NewsStory) -> Bool {
return lhs.id == rhs.id
}
到此為止,完美解決問題!我們實現Hashable版本比Swift自動生成的方法的版本更快,因為我們的hash函數只計算了ID的hash值,而Swift的版本計算了所有屬性的hash值。
示例中我們只使用了id這個屬性值,但是你在項目中也可以使用更多的屬性來保證你的對象是不同的。
最終NewsStory
代碼如下所示:
struct NewsStory: Hashable {
var id: Int
var title: String
var date = Date()
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func ==(lhs: NewsStory, rhs: NewsStory) -> Bool {
return lhs.id == rhs.id
}
}
在我們的文章結束之前,需要提醒一點, 其實是Rob Napier的提醒:相等意味着可替換——任何兩個相等的對象在代碼中都可以相互替換。如果你只比較了id
,那就意味着"如果兩個對象有相同的id,但是其它屬性是不同的,我不關心其它屬性是什么樣的,算法可以自由的返回其中的任意一個。"
最后,也是最重要的一點:如果兩個對象相等(因為自定義的==
返回true),那么Swift會自由選擇。Swift可能總是選擇第一個對象,也可能總是選擇第二個對象,或者每次隨機選擇兩個中的一個——這種表現在未來的Swift版本中可能會發生改變。記住這點,因為我們告訴Swift兩個對象是相同的,才會發生這個問題,如果關於對象的選擇對你來說很重要,你需要注意這個問題。