Swift 中的泛型使用

edithfang發表於2014-10-19
恭喜你,你可真棒!在不到半個時間裡,你已經通過宣傳IOS 8盛宴成功解鎖了第一個Swift教程!它是Swift系列教程的一個縮水版,就先給你偷看一眼了好了,希望你能夠喜歡!

如果你已經動手寫過Swift的程式,相信你已經瞭解了Swift語言的知識,比如如何寫類(class)和結構體(struct)。但Swift可沒這麼簡單,呵呵呵。這篇教程主要講述Swift的一個強力的特性:泛型。這個特性在很多程式設計語言裡都非常受歡迎。

對於型別安全(type-safe)語言,一個常見的問題就是如何編寫適用於多種型別輸入的程式。想象一下,兩個整型數相加和兩個浮點數相加的程式看起來應該非常類似,甚至一模一樣才對。唯一的區別就是變數的型別不同。

在強型別語言中,你需要去定義諸如addInts, addFloats, addDoubles 等方法來正確地處理引數及返回值。

許多程式語言已經解決了這個問題。例如,在C++中,使用Template來解決。而Swift,Java和C#則採用了泛型來解決這個問題。泛型,也是這篇文章要重點介紹的。

在這篇文章中,你將會學到Swift中如何使用泛型,也許你已經接觸過,也許沒有,不過沒關係,我們會來一一探索。然後,我們會建立一個Flicker圖片搜尋應用,這個應用使用了自定義的泛型資料結構來儲存使用者搜尋的內容。

備註:本文假設你已經對Swift有基本的瞭解或者有過Swift開發經驗。如果你第一次接觸Swift或者對Swift不是太瞭解,建議你首先閱讀下other Swift tutorials

泛型介紹

也許你不知道這個術語,但相信你已經在Swift中見到它了。Swift中的陣列和字典型別就是使用泛型的經典例子。

Object-C開發者已經習慣使用陣列和字典去儲存多種資料型別。這種方式提供了很大的靈活性,但是誰又能知道一個API返回的陣列裡面到底是啥(資料型別)呢?你唯一能做的就是檢視文件或者檢視(方法的)變數命令(這也是另外一種文件喲!)。即使你檢視了文件,你也不能保證程式在執行期不產生bug或者其他異常。

相比Object-C,Swift中的陣列和字典都是型別安全的。一個Int型陣列只可以儲存Int而不可以儲存String。這意味著你不用再檢視文件啦,編譯器就可以幫你做型別檢查,然後你就就快可以愉快地coding了!

例如,在Object-C的UIKit中, 在自定義的View裡面處理觸控事件可以這麼寫:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
上述方法裡面的set只可以儲存UITouch例項, 因為文件裡面就是這麼說的。由於這個集合裡面可以放任何物件,所以你需要在程式碼裡面進行型別轉換,也就是說把touches裡面的物件轉為UITouch物件。

當前Swift的標準庫裡面沒有定義集合物件,但是你可以使用陣列來代替集合物件,你可以用swift重寫上面的程式碼:
func touchesBegan(touches: [UITouch]!, withEvent event: UIEvent!)
上面的程式碼明確告訴你 touches陣列只可以包含 UITouch例項, 否則編譯器就會報異常。這樣一來,你就再不用去做那煩人的型別轉換了,因為編譯器給你做了型別安全檢查,保證陣列裡面只允許有 UITouch物件。 

簡要說來,泛型為類提供了一個型別引數。所有的陣列都有相同的作用,即按順序儲存變數的數值,泛型陣列除了多了一個型別引數之外,沒有其他的不同之處。或許這樣想更容易理解:你將要應用在陣列上的各種演算法和儲存的數值型別無關,因此這些演算法對於泛型陣列和非泛型陣列都適用。

既然你已經明白了泛型的基礎知識和用法,那我們就開始把它應用在一個具體的例子上吧。

泛型例項

為了測試泛型,你將會編寫一個在Flickr上搜尋圖片的應用。

首先請下載這個程式雛形,並儘快熟悉裡面主要的類。其中Flickr類用於和Flickr的API互動。請注意這個類裡面包含了一個API key(通常用於使用者授權—譯者注),但如果你想要擴充套件這個應用的話可能需要用自己的key,註冊請點我

構造並執行這個應用,你會看到這個:



好像什麼都沒有?別急,用不了多久你就可以讓它幫你抓取可愛的喵圖了!

有序字典(原文Ordered Dictionaries)

你的應用會根據每個使用者的查詢情況下載圖片,按照圖片被搜尋到的頻率由高到低排序並顯示。

但如果使用者對同樣的關鍵字搜尋了兩次會怎樣?如果這個應用能顯示上次搜尋的結果就好了。

或許用陣列來實現這個功能也行得通,但為了學習泛型,你將會使用一個全新的資料結構:有序字典。

和陣列不同的是,包括Swift在內地很多程式語言和框架都不保證集合(sets)和字典(dictionaries)的資料儲存順序。有序字典和普通的字典類似,不同之處在於它的key是有序的。你將會用這個特性,根據搜尋關鍵字按順序儲存搜尋結果。這樣儲存的好處是可以快速查詢並更新圖片列表。

一個草率的想法是自定義一個資料結構處理有序字典。但是你需要更加有前瞻性才行!你必須考慮到如何讓你的應用在未來幾年內都能正常工作!因此在這裡使用泛型再合適不過了。

初始資料結構

點選“檔案\新建\檔案...”新建一個檔案,並選擇“IOS\Source\Swift File”。點選“下一步”並把這個檔案命名為“OrderedDictionary”。最後,點選“建立”。

你會得到一個空的Swift檔案,加這樣一段程式碼進去:

struct OrderedDictionary {  }
到現在為止應該都沒有什麼問題。通過語義可以看出這個物件是一個結構體。

注意:總之,值的語義可以想象為“複製、貼上的行為”,而不是“分享、參考的行為”。值的語義帶來一系列的好處,例如不用擔心一段程式碼無意地修改你的資料。瞭解更多,點選"Swift by Tutorials"的第三章節:類和結構體。

現在你需要將其一般化,以便它能夠裝載你需要的任何型別的資料。通過下列改變你對Swift中“結構”的定義:
struct OrderedDictionary<KeyType, ValueType>
在尖括弧中的元素是通用型別的引數。KeyType和ValueType不是他們自身的型別,而是你可以使用在結構裡定義取代的型別。現在就簡潔清新許多了!

最簡單的實現一個有順序的字典是保持一個陣列和一個字典。字典中將會裝載衍射,而陣列將裝載keys的順序。

在結構體內部的定義中,加入以下的程式碼:
typealias ArrayType = [KeyType]typealias DictionaryType = [KeyType: ValueType] var array = ArrayType()var dictionary = DictionaryType()
這樣宣告有兩個目的,就像上例描述的,有兩種型別的用於給已經存在的型別的取新的名稱的別名。在這,你將分別地為後面的陣列和字典賦值了別名。宣告別名是將複雜型別定義為更短名稱的型別的一種非常有效的方式。

你將注意怎麼樣從結構體中定義用“KeyType”和“ValueType”的引數型別中替換型別。上例的"KeyTypes"是陣列型別的。當然這是沒有這樣的型別的“KeyType”;當在一般的例項化時,將替代Swift像對OrderedDictionary的型別的一切型別通過。

就因為這樣,你將會注意到編譯錯誤:
Type 'Keytype' does not conform to protocol 'Hashable'
或許你會詫異怎麼會這樣?請再觀察下Dictionary的繼承者:
struct Dictionary<KeyType: Hashable, ValueType>
除了在KeyType之後的HashTable, 其他的都和OrderedDictionary的定義特別的相似。在分號後面為KeyType宣告的Hashable,一定符合Hashable的協議。這是因為字典需要為hash key實現。

用這種方式約束泛型引數是非常常見的。例如,你想要依據你的應用使用引數做什麼,來約束值的型別以,確保相等性、可列印性協議。

開啟OrderedDictionary.Swift,用下例來取代你對結構體的定義:
struct OrderedDictionary<KeyType: Hashable, ValueType>
這樣為OrderedDictionary宣告KeyType,必須符合Hashable。這就意味著,無論KeyType變成什麼型別,都可以接受為沒有宣告的字典的KEY。

這樣,檔案再次編譯,將不會報錯!

Keys, Values 和所有的這些趣事

如果不能為字典新增值,那麼字典有什麼作用了?開啟OrderedDictionary.swift,在你的結構體定義中新增以下函式:
var adjustedIndex = index
   // 2
  let existingValue = self.dictionary[key]
  if existingValue != nil {
    // 3
    let existingIndex = find(self.array, key)!     // 4
    if existingIndex < index {
      adjustedIndex--
    }
    self.array.removeAtIndex(existingIndex)
  }   // 5
  self.array.insert(key, atIndex:adjustedIndex)
  self.dictionary[key] = value
   // 6
  return existingValue}

下面介紹一些新的特性。讓我們一步一步來介紹:
  • 插入一個新物件的方法,insert(_:forKey:atIndex),需要三個引數:一個特別的key的值,插入一對key-value的索引。這是你之前沒有注意到的一個關鍵字:改變。
    結構體的設計是預設不變的,這意味著通常你在例項化的方法中,不能改變結構的成員變數。這十分有限,你能新增改變的關鍵字,並告訴編譯器這個方式在結構體中是允許改變的。這將幫助編譯器做出決定什麼時候複製結構體(他們是寫時複製的),也有助於API的編檔。
  • 你為字典的索引器輸入一個如果已經存在,那麼返回已存在的值的key,這個插入方法模擬字典更新值相同的行為,因此為這個值保持已經存在的值。
  • 如果這有一個已經存在的值,只有這樣函式才能為這個值在陣列裡找出索引。
  • 如果這個已經存在的key在插入索引的之前,這時你需要調整插入的索引,因為你需要移除已經存在的key。
  • 你將適當地更新陣列和字典。
  • 最後,你返回已存在的值,當這或許沒有已存在的值,這個函式返回一個可選的值!
現在你可以為字典新增移除值?

像下列對OrderedDictionary結構體的定義的函式:
// 1mutating func removeAtIndex(index: Int) -> (KeyType, ValueType){
  // 2
  precondition(index < self.array.count, "Index out-of-bounds")   // 3
  let key = self.array.removeAtIndex(index)   // 4
  let value = self.dictionary.removeValueForKey(key)!   // 5
  return (key, value)}
現在再讓我們一步一步分析:

1.這是改變結構體狀態的函式,removeAtIndex的名稱需要和陣列的方法匹配。恰當的時候,考慮使用映象系統庫中API是不錯的選擇。這樣幫助開發者在他們的工作平臺裡,非常容易地使用你的API。
2.首先,你需要檢查索引,觀察他們是否是在大量的陣列裡。嘗試著從未宣告的陣列中移除越位的元素,將會導致超時錯誤,所有在這時檢查將會更早符合這樣的情況。你或許在Objective-C中使用斷言函式;在Swift中斷言也是可使用的。但是前提是在釋放的工程中是活動的,否則你執行的應用的將會終止。
3.接著,當同時從陣列中移除值時,你在給定的索引中陣列中獲得值。
4.然後,你從字典中為這個key移除的值,同時也會返回這個值。或許在給出的key中,字典也沒有相應的值,所以removeValueForKey返回一個可選的。這種情況下,你知道字典將會為給出的key,包含一個值,因為這是唯一的自己給字典新增值的方法--insert(_:forKey:atIndex:),這時你可以選擇使用“!”,表明這將會有正義感值。
5.最後,你在一個元組返回的key和value。陣列的removeAtIndex和字典的removeValueForKey是一樣的返回已存在的值功能。

值的讀取跟寫入

把值寫入字典(dictionary)是沒問題了, 可是這樣還不夠! 你還需要實現一個方法(method) 從字典中讀出相應的值.

開啟 OrderedDictionary.swift 檔案, 然後把下列程式碼新增到結構定義(struct definition)當中, , 就放在 thearrayanddictionaryvariable 宣告的下面:
var count: Int {
  return self.array.count}
這個常用的屬性, 用來算出字典裡面有幾條記錄. 只要返回陣列的 count 屬性的中值就可以了!

接下來, 就是如何訪問(Access)字典中的記錄了(Element). 我們可以通過下標(Subscript)來訪問, 程式碼如下:
let dictionary = [1: "one", 2: "two"]let one = dictionary[1] // Subscript
下標的語法我們會用了, 但是如果是我們自己定義的類那該怎麼用呢? 好在 Swift 支援在自定義類裡頭新增這項功能. 而且實現起來也不復雜. 

把下列程式碼新增到結構定義的底部:
// 1
subscript(key: KeyType) -> ValueType? {
  // 2(a)
  get {
    // 3
    return self.dictionary[key]
  }
  // 2(b)
  set {
    // 4
    if let index = find(self.array, key) {
    } else {
      self.array.append(key)
    }     // 5
    self.dictionary[key] = newValue  
  }
}
我們詳細解釋下這段程式碼:
  • 上面程式碼註釋中標有 1 的那一段:跟 func 和 var 類似, subscript 也是個關鍵字, 通過它定義下標.  引數 key 是出現在中括號中的那個物件.
  • 註釋中標有 2 的那一段: 下標由 setter 跟 getter 兩部分組成. 本例同時定義了 setter (程式碼中的 set  ) 跟 getter (程式碼中的 get ). 當然, 不是每個下標都要同時定義 setter 跟 getter. 
  • 註釋中標有 3 的那一段: getter 比較簡單, 只要通過引數 key, 在字典中找到相應的值即可. 字典返回的是可選值(optinal), 如果 key 不存在, 該值為 nil.
  • 註釋中標有 4 的那一段: setter 就複雜些. 首先要檢測這個 key 在有序字典裡面是不是已經存在. 如果不存在, 則把 key 新增到陣列中. 由於我們需要把 key 新增到陣列的尾部, 所以這裡呼叫的是 append 方法.
  • 註釋中標有 5 的那一段: 把值新增到字典中. 這裡用隱性命名的變數 newValue 獲取傳遞過來的值.
就像用 Swift 自帶的字典類的下標那樣去, 你可以通過 key 來查詢某個值. 可是如果我們需要像訪問陣列那樣, 用下標索引(index)訪問某個值, 該怎麼辦呢? 既然是有序字典, 沒有道理不能通過下標索引一個一個的按順序訪問.

結構體跟類可以定義多個引數型別不同的下標(subscript). 把下列程式碼新增到結構定義的底部:
subscript(index: Int) -> (KeyType, ValueType) {
  // 1
  get {
    // 2
    precondition(index < self.array.count, 
                 "Index out-of-bounds")     // 3
    let key = self.array[index]     // 4
    let value = self.dictionary[key]!     // 5
    return (key, value)
  }}
這段程式碼跟前面那段類似, 不同的是引數型別變成了 Int. 因為我們現在要實的功能是現像陣列那樣, 使用下標索引訪問有序字典. 不過這次返回的是由 key 跟 value 組成的一個元組(tuple). 因為有序字典就是由這樣一個一個的元組構成的.

下面具體解釋下這段程式碼:
  • 這個下標只定義了 getter. 當然你也可以把 setter 加上. 不過要注意先檢查 index 會不會越界.
  • index 的值不能超出陣列的界限, 也就是字典元組的個數. 我們可以利用 precondition 提示開發人員, 程式出現越界訪問.
  • 用 index 從陣列中讀出 key.
  • 再用 key 從字典中讀取 value. 需要注意的是, 由於陣列中的每一個 key 跟字典的 value 是一一對應的, 所以這裡使用符號 ! (unwrapped) 對讀出來的 value 拆包.
  • 最後, 返回一個包含 key 和 value 的元組.
挑戰: 為上面那個下標實現 setter . 可以參考前面的例子.

提示 1

注意, newValue 是個包含 key 跟 value 的元組.

提示 2

下列程式碼可以將值從元組中提取出來:
 let(key, value) = newValue

也許你會好奇, 如果 KeyType 是 Int 型的, 會出現什麼問題? 使用泛型的好處是, 不管是什麼型別, 只要能算出雜湊值(hashable)的就行,  所以 Int 當然也能用.  問題是, 當 key 也是 Int 型的時候, 這倆個下標該怎麼區分呢?

這就需要我們給編譯器提供更多的型別資訊. 讓它知道在什麼時呼叫哪個下標. 比如我們定義的這兩個下標, 返回的型別不一樣. 如果你用 key-value 型別的元組給它賦值, 編譯器就會自動呼叫那個陣列式(array-style )的下標.

在專案中測試

讓我們在實際專案中,實驗編譯推斷使用的下標函式,以及一般情況下,OrderedDictionary是怎麼工作的。

通過點選"檔案"、"建立"、"檔案",新建一個專案,依次選擇"IOS"、"Source"、"Playground",再點選下一步。然後點選建立。

你必須得這樣操作:複製和貼上OrderedDictionary.swift整體到新建的專案中。因為不能在寫教程時你的應用模型中”看見”程式碼

注意:這有一個解決方法,可以取代複製、貼上的方式。如果你需要將你應用的程式碼加入到一個框架中,你的專案將接受的你程式碼,就像Corrine Krych指出的這樣。

現在,在你的專案底部加入下列的程式碼:
var dict = OrderedDictionary<Int, String>()dict.insert("dog", forKey: 1, atIndex: 0)dict.insert("cat", forKey: 2, atIndex: 1)println(dict.array.description 
        + " : " 
        + dict.dictionary.description) var byIndex: (Int, String) = dict[0]println(byIndex) var byKey: String? = dict[2]println(byKey)
在側欄中(或者通過檢視/助理編輯/顯示助理編輯/),你將可以看到println()函式輸出的內容:



在這個例子中,字典有一個整數型的key,所以編譯器會審查被使用分配決定使用哪個下標變數的型別。如果被使用的下標是一個(Int, String)的byIndex,編譯器會匹配期望的返回值型別,使用陣列型別的索引的下標。

如果你想從一個 byIndex 或者 byKey的變數中,移除型別的定義。編譯器將會報錯,表明編譯器不知道使用哪一個下標。

小貼士:由於編譯器是按照型別推理來工作的,所以需要明確地表示出型別。當存在多個有相同的爭議的返回值型別的函式時,呼叫者需要具體化。需要注意:Swift中函式,可以“建-破”改變。

通過在專案中,對有順序的字典的實驗中,你可以發現他的工作原理。在重返app之前,嘗試從中新增、移除、以及改變key和value的型別。現在,你可以在你的順序字典中讀、寫操作!但要小心你的資料結構。現在你可以通過app來感受其中的樂趣了!

新增圖片查詢

現在是時候讓你回過頭來注意手中的app了。開啟 MasterViewController.swift。在兩個@IBOutlets 的下面,新增下列變數的定義:
var searches = OrderedDictionary<String, [Flickr.Photo]>()
你或許在困惑,為什麼Flickr.Photo的型別中有個句號。那是因為Photo是在Flickr類的內部定義的類。在Swift中,這樣的層次結構是非常有利的。它將有助於類的名稱簡短化。在Flickr的內部,你可以單獨使用Photo類,因為上下文關係告訴了編譯器這是什麼。這是順序化字典查詢使用者訂閱的Flickr的功能。真像你看到的,包含查詢的字串,和Flickr.Photo陣列,或是從Flickr API 中返回的照片。注意,你在尖括號裡給出的key和value,將成為在具體實現中KeyType和ValueType的引數型別。

接下來,找到表格檢視資料來源的tableView(_:numberOfRowsInSection:)方法,然後把它改為如下所示:
func tableView(tableView: UITableView, 
               numberOfRowsInSection section: Int) -> Int{
  return self.searches.count}
這個方法使用有序字典來告訴表格檢視有多少行。接著,找到表格檢視資料來源的tableView(_:cellForRowAtIndexPath:)方法並把它改為如下所示:
func tableView(tableView: UITableView, 
               cellForRowAtIndexPath indexPath: NSIndexPath)
              -> UITableViewCell{
  // 1
  let cell = 
    tableView.dequeueReusableCellWithIdentifier("Cell", 
      forIndexPath: indexPath) as UITableViewCell   // 2
  let (term, photos) = self.searches[indexPath.row]   // 3
  if let textLabel = cell.textLabel {
    textLabel.text = "\(term) (\(photos.count))"
  }
  return cell}
這是你在這個方法中所做的:

1. 首先,從UITableView中挪出一個單元格。你需要把它直接轉換為UITableViewCell,因為dequeueReusableCellWithIdentifier仍舊返回AnyObject(id in Objective-C),而不是UITableViewCell。或許在將來,蘋果公司會利用泛型重寫這部分API。
2. 接著,用你給的下標索引從指定的行獲取Key和Value,
3. 最後,適當地設定單元格的文字標籤並且返回當前單元格。

現在讓我們嚐嚐鮮。找到UISearchBarDelegate 的擴充,就像下列的程式碼一樣,改變單例方法。
func searchBarSearchButtonClicked(searchBar: UISearchBar!) {
  // 1
  searchBar.resignFirstResponder()   // 2
  let searchTerm = searchBar.text
  Flickr.search(searchTerm) {
    switch ($0) {
    case .Error:
      // 3
      break    case .Results(let results):
      // 4
      self.searches.insert(results, 
                           forKey: searchTerm, 
                           atIndex: 0)       // 5
      self.tableView.reloadData()
    }
  }}
當使用者點選查詢按鈕時,這個方法將會被呼叫。下列就是在這個方法中,你正做的事:

1.你第一反應是放棄使用查詢框以及鍵盤。
2.然後,你又會使用搜尋框查詢你輸入的文字,為查詢的文字而使用Flickr類來尋找。Flickr的查詢方法是:查詢術語,關閉執行查詢成功或者失敗。通過引數關閉:要不是錯誤,那就是結果。
3.在錯誤的情況下,不會發生任何事。但是你可以通過使用警報來提示錯誤,但是現在我們可以簡化這樣的操作。程式碼需要在這時暫停會兒,告訴Swfit編譯器你的錯誤沒有任何反應的動機。
4.如果查詢有效,將會在查詢結果中顯示相關的值。你將查詢的術語作為key加入到順序字典中。如果已經在字典中存在了,將會把他放入到list的頂部,然後用最後的結果更新整個字典。
5.最終,由於你有新的資料,將再次載入table view。

哇!你的app將可以用於查詢圖片了!

構建並執行app,做幾次查詢。你將會看到下面這樣的一些東西:



現在再用與之前搜尋詞不同的另外一個進行查詢. 你將會看到它跳到了頂部:



選擇一個查詢結果點選去,你會發現它並沒有顯示照片。現在是時候修復這個問題了!

給我看照片!

開啟MasterViewController.swift 並找到 prepareForSegue. 將它修改成下面這樣:
override func prepareForSegue(segue: UIStoryboardSegue, 
                              sender: AnyObject?){
  if segue.identifier == "showDetail" {
    if let indexPath = self.tableView.indexPathForSelectedRow()
    {
      let (_, photos) = self.searches[indexPath.row]
      (segue.destinationViewController         as DetailViewController).photos = photos    }
  }}
這是用了同建立單元項時訪問被排好序的查詢結果字典一樣的方法. 儘管沒有使用關鍵詞(檢索詞), 你也可以用下劃線來顯示出元組的這個部分不需要被繫結到一個本地變數.

構建並執行app,做一次查詢然後點進去。你會看到像下面這樣的東西:



你好,小貓貓! 你有沒有想要發出快樂的呼聲呢? :]

從此何去何從?

這裡有來自Swift泛型教程的 完整示例專案.

恭喜了,你已經學習許多有關泛型的東西! 此外,你還學習了諸如下標,結構以及可變行為,先決條件等其它有趣的東西.

如果你想了解更多有關泛型的東西, 可以翻翻Swift教程的完整章節, 在裡面我把這個示例帶入更深入的層面,同時也涵蓋了泛型函式和協議.

我希望看到你在未來的app中利用泛型的力量來避免程式碼重複,並使得你的程式碼更具可重用性. 如果在過程中你有任何疑問或者觀點,請加入下面的論壇討論中來吧!
相關閱讀
評論(1)

相關文章