在 Swift 中實現字典

2021-09-19 03:19:07 字數 4641 閱讀 2307

雖然 swift 原生的字典型別實現的很複雜(毫無疑問是為了效能),但是我們可以利用 swift 提供的工具寫出漂亮簡潔的實現。我們從乙個簡單的實現開始,並且逐步新增功能。

我們簡要看一下字典的工作原理:它通過任意型別的關鍵字來設定和獲取值。這些值常常儲存在乙個陣列中,當然也可以儲存在樹型結構中。由於我還不太清楚以樹作為字典儲存結構的工作原理,所以這篇文章中我們主要探索乙個由陣列儲存值的字典。

由於我們字典中的值是由陣列儲存,所以我們需要將給定的關鍵字轉換成整數,然後讓這個整數落在陣列範圍內。這兩個方法乙個是雜湊函式,乙個是取模操作。通過雜湊函式,我們可以將關鍵字(通常是字串,也可以是遵循hashable協議的任意型別)轉換成乙個數值,然後根據陣列的長度,通過取模操作我們會得到乙個固定位置(consistent slot)來設定和獲取值。

譯者注: 這裡的 consistent slot 是指,字典的 key,通過這種 hash 和取模運算,每次都會得到相同的數值,從而確保相同的 key 值,每次取得的 value 總是保持一致性。

我從mike ash的文章 讓我們構建 nsmutabledictionary 獲取了一些靈感,特別是重新計算陣列容量的規則。

讓我們開始吧。我們知道keyvalue都應該支援泛型,而且key必須遵循hashable協議。遵循hashable協議的也一定同時遵循equatable協議。(譯者注,hashable協議繼承自equatable協議)

struct dictionary

set

}}

這是我們型別的基本結構。我們知道我們的陣列必須是支援泛型的,但是還不知道具體的值是什麼。由於兩個不同的關鍵字經過雜湊和取模操作後可能會指向陣列的同乙個位置,這樣就會造成衝突,因此每個佔位物件需要支援儲存多個值。現在讓來我們設計placeholder

struct placeholder
這個物件儲存很多經過雜湊和取模操作後指向字典同一位置的鍵和值。如果我們很好的設計了字典,那麼每個placeholder包含的鍵值對將不會超過乙個,但是超過乙個的情況也會發生。乙個比較好的實現是用乙個鍊錶來儲存placeholder的屬性values。我將留作乙個練習給讀者。

現在我們大體知道了placeholder是什麼樣的,接下來我們看看我們的儲存器是什麼樣的。

private var storage = array(count: 8, repeatedvalue: placeholder())
初始時我們給陣列乙個隨機的大小。最好選擇 2 的整數冪,因為對 2 的冪取模要比其他數更快。除非我們實現如何重新計算陣列容量,否則取多大都沒有關係。array(count:repeatedvalue:)構造方法使得陣列中的每個位置都有乙個可以新增值的佔位物件。

為了設定字典的值,我們需要對鍵進行雜湊(然後取絕對值,因為雜湊有時會返回負值),然後根據陣列大小進行取模操作。

set
在真正給字典新增值之前,我們需要

a)確保新值(newvalue)不為 nil

b)找到在position位置的佔位物件

c)將鍵和值新增到佔位物件中。

set 

let position = abs(key.hashvalue) % storage.count

}

這就是基本滿足我們需求的設定方法的實現。(我忽略掉了一些小的細節,比如當你試著對同乙個鍵設定兩次時會發生什麼。我們很快會處理這種情況。)

取值的過程相當簡單。對鍵進行雜湊,取絕對值,取模操作,然後我們獲取了在那個位置上的佔位物件。我將會把從佔位物件中獲取值的過程交給佔位物件自己去做。

get
真正神奇的是在下面這個方法。我們需要找到第乙個包含那個鍵的鍵值對。在 swift 3 中我們可以使用sequencetype協議中的first(where:)方法來實現,但目前我們使用普通的寫法lazy.filter( /* block */ ).first來獲取鍵值對。

func firstvalue(matchingkey key: key) -> value? ).first

//...

}

一旦我們拿到表示鍵值對的元組,我們就可以直接呼叫.1獲取對應的值。

func firstvalue(matchingkey key: key) -> value? ).first?.1

}

這幾乎是dictionary的基本實現了。23 行 swift **。所有**在這個 gist 中。

還有幾個有意思的事情還沒有實現。首先,需要有惰性求值的generate(),這樣的話dictionary就遵循sequencetype協議了。

譯者注: 遵循sequencetype協議之後,就可以使用for (key, value) in的方式來遍歷字典了

extension dictionary: sequencetype ).generate()

}}

接下來是刪除鍵。首先,從佔位物件中刪除:

extension placeholder )

}}

然後從字典中刪除

extension dictionary 

}

在設定新值時先呼叫remove(key:)。這樣確保同乙個鍵不會對映到不同的值上。

最後,我們來看看如何重新計算陣列容量。當字典包含非常多物件時,它的實際儲存結構需要調整自身大小,可以把「非常多物件」看成乙個大於2/3或者3/4的負載係數(物件數量除以陣列長度)。我選擇 0.7。

extension dictionary 

var count: int ).count

}var currentloadfactor: double

}

count的實現還是懶求值。理想情況下,dictionary會記錄有多少物件被新增和被刪除,但實際上很難記錄)

mutating func resizeifneeded() 

}

這就是 swift 的值語法非常奇怪的地方。mike ash在 構建nsmutabledictionary 文章中,建立了乙個固定大小的字典,並且把它封裝成乙個可變大小的字典。當需要調整大小時,他建立乙個新的固定大小的字典(大小加倍),然後手動的把所有元素拷貝到新的字典當中。

在 swift 中我們不必如此。swift 中的結構體物件賦值給乙個變數會進行一次完整拷貝。

let olddictionary = self

//...

一旦我們拷貝完字典,我們可以重置storage變數為原先陣列大小的兩倍。(大小加倍確保陣列大小仍然是2的冪。)

//...

storage = array>(count: size*2, repeatedvalue: placeholder())

//...

一旦設定完成,字典會變成空的,我們必須把olddictionary的所有值拷貝到當前字典中:

//...

for (key, value) in olddictionary

這是完整的resizeifneeded函式:

mutating func resizeifneeded() 

}}

在 swift 的結構體中,self可以訪問當前型別的值和函式,但是它也是可變的。你可以對它設定新值,拷貝它,或者就把它當成另乙個變數的引用。

兩周前 我開玩笑說 swift 是比 objective-c ,ruby 或者 python 更動態的語言。因為你可以改變它的錯誤行為,但是這裡有另一種情況,即你可以在 swift 中修改一些在 objective-c 中不能修改的東西:self本身的引用。我們本可以在該方法中這樣寫self = dictionary(size: size*2),這在 swift 是絕對有效的。對於那些覺得物件標識是最重要的物件導向開發者而言,這非常奇怪。

包含排序,刪除,調整陣列大小的完整的實現可以在 gist 中找到。除了count/generate()懶求值的實現,我非常喜歡這個小工程的表達方式。

Swift 中實現字典

雖然 swift 原生的字典型別實現的 很複雜 毫無疑問是為了效能 但是我們可以利用 swift 提供的工具寫出漂亮簡潔的實現。我們從乙個簡單的實現開始,並且逐步新增功能。我們簡要看一下字典的工作原理 它通過任意型別的關鍵字來設定和獲取值。這些值常常儲存在乙個陣列中,當然也可以儲存在樹型結構中。由於...

在 Swift 中實現單例方法

我們通常在進行開發的時候,會用到乙個叫做 單例模式 的東西。相信大家也都對這種模式非常熟悉了。而且單例的使用在平時的開發中也非常頻繁。比如我們常用到的nsuserdefaults.standarduserdefaults 在 swift 中我們如何實現單例模式呢?如果你曾經對 objective c...

Swift中的陣列和字典

swift對陣列和字典的定義和使用語法,體現了現代語言的特色。熟悉後會感覺更加簡練和易用。定義和宣告 var a array 等同於 var a string let animals giraffe cow doggie let animal animals 4 crash 陣列越界 for ani...