如何在Set集合中避免重復元素


文章翻譯自 Avoiding near-duplicates in sets, 作者Paul Hudson @twostraws是一名優秀的Swifter。 這是我第一次翻譯,可能有翻譯不到位的地方,如果有任何問題,歡迎反饋。學習學習再學習,加油💪!

img

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兩個對象是相同的,才會發生這個問題,如果關於對象的選擇對你來說很重要,你需要注意這個問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM