第四章 Generics - 泛型

現在,你已經知道了Swift中如何實現基本的類和結構,但Swift的強大遠不止如此。本章要講的是Swift的另一個非常強大的語法:泛型。

對於類型安全的語言來說,都有一個常見的問題。想寫一個作用於一種類型的代碼,但同時又能對另一種類型起作用。想象下,一個函數添加兩個整數,一個函數添加兩個浮點數。這兩個函數看起來其實是一樣的,唯一的區別在變量的類型不同。

在強類型語言中,你可能需要分別定義函數如addInts,addFloats,addDoubles等等。每個函數都能傳入正確的參數返回正確的類型。

許多語言都有了解決這個問題的方案,例如c++用的是模板。Swift和java一樣,使用泛型。在這一章中,你可以瞭解到泛型的相關內容,然後開發一個網絡相冊照片的搜索應用。自定義通用的數據結構來記錄用戶的搜索詞。

現在開始吧!

Introducing generics - 引入泛型

你可能還不認識泛型,但是在本書中你已經看見過並使用過了。數組和字典便是經典的安全類型語言的泛型案例。

Object-C開發員習慣在一個相同的集合數組或字典中保存不同類型的對象。這確實提供了不小的靈活性,但是你怎麼知道從一個api返回的數組都保存了些什麼?你只能查看文檔或是變量名來猜測。即使是文檔(沒有丁點錯的文檔),也沒有辦法預防某些集合在運行中的意外情況而導致裏面保存的數據類型被改變。

Swift類型化了數組和字典 。讓int數組只能持有int,絕不會包含有一個字符串。這意味着你可以編寫代碼文檔讓別人瞭解你的數組,並讓編譯器給你做數組元素的類型檢查。

例如,在Object-C的UIKit中,下面的方法在自定義的view中處理觸摸手勢:

(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; 

set集合在該方法中已知的只有UITouch實例,但也只是因爲文檔是這麼告訴我們的。沒有辦法阻止其他的對象進入這個集合,你通常需要將touches實例化有效的作爲UITouch的對象。

Swift沒有像上面那樣定義一個set集合,然而當你使用一個數組的集合時,你可以將上面的方法修改爲:

func touchesBegan(touches: [UITouch]!, withEvent event: UIEvent!) 

這便告訴你touches數組只包含有UITouch實例,如果通過別的代碼調用此方法則編譯器拋出錯誤。編譯器不僅控制了在touches數組中的類型,而且你不再需要將數組元素UITouch實例化。

總的來說,泛型提供了作爲參數的類型限制。所有的數組採取同樣的方式,泛型會控制參數的值類型,將值保存在一個列表中。如果你這樣想會發現泛型很有用:因爲你的數組中不會有不知道的類型元素,所以所有的有值的數組,你都可以放心的使用。

現在你已經有了對泛型的基本認識與使用,是時候將他們應用到一個具體的場景中了。

Generics in action - 泛型的運用

泛型練習,需要建一個應用程序用於搜索圖片。爲了你能快速進行學習,資源文件夾中提供了開始練習的項目工程。編譯並運行代碼,可以看到運行如下:
這裏寫圖片描述

界面上貌似毛都沒有一根,別擔心,你馬上就會有照片在上面展示。

Ordered dictionaries - 序列化字典

你的應用將爲每個用戶查詢並下載圖片。最近的搜索結果將在列表的最頂部顯示。但如果你的用戶同一個搜索詞用了兩次呢?如果應用程序能用最新返回的結果來替換掉列表頂部最近的舊數據就好了。你可以使用數組結構來開發,但是爲了學習泛型,我們將創建一個新的集合:有序的字典。

在許多的語言(包括Swift)和框架中,sets集合和字典都不像數組一樣可以以順序保存。有序字典其實就像普通的字典,不過定義的key是一個有序的值。你可以使用這個功能來存儲搜索詞對應下的搜索結果,讓你能快速的找到結果,同時也可以維護訂單tableView的視圖。

The initial data structure - 初始化數據結構

添加一個新的文件點擊File\New\File… ,然後選擇iOS\Source\Swift File. 再點擊Next並給文件命名爲OrderedDictionary 。

你將看到一個空的Swift文件,添加如下代碼:

struct OrderedDictionary { } 

到目前爲止都還沒有什麼令人驚奇的。這個對象應該是個結構因爲他是個值語義的對象。詳見第三章類和結構的區別。

現在你需要讓他可以容納任何你想要的類型的值,修改結構定義如下:

struct OrderedDictionary<KeyType, ValueType> 

在尖括號裏的元素是泛型的類型參數。KeyType和ValueType本身不是類型,而是成爲你在定義結構時的類型參數。等下你就會明白。

最簡單的有序字典的實現方式是使用數組和字典。字典負責鍵值對的映射,數組負責key鍵的順序。

在結構的定義中添加如下代碼:

typealias ArrayType = [KeyType]
typealias DictionaryType = [KeyType: ValueType] 
var array = ArrayType()
var dictionary = DictionaryType() 

這裏聲明瞭兩個屬性,爲了描述方便,給兩個類型設置了別名。在這裏,你給的數組和字典別名分別支持數組和字典。類型別名是個偉大的構思,讓你可以給一個複雜的類賦予一個簡短的名字。

注意你可以使用結構的類型參數KeyType和ValueType。這個數組是一個KeyType的數組,當然,其實並沒有一個類型叫KeyType。而是Swift把他處理爲在實例化泛型OrderedDictionary的任何一種類型。

同時,你會看到編譯器錯誤:
這裏寫圖片描述

這可能有點意外,看下系統自帶的字典是如何實現的。

struct Dictionary

struct OrderedDictionary<KeyType: Hashable, ValueType> 

這聲明瞭KeyType在OrderedDictionary中必須符合哈希。也就意味着任何類型的KeyType都可以被接受作爲字典的關鍵字。

現在編譯文件不會再報錯了。

Keys, values and all that jazz - 關鍵詞,值的處理

如果你不添加值怎麼使用字典呢?打開OrderedDictionary.swift 並將下面的函數添加到你定義的結構中

  // 1
    mutating func insert(value: ValueType, forKey key: KeyType, atIndex index: Int) -> ValueType?
    {
       var adjustedIndex = index

        let existingValue = self.dictionary[key]
        if existingValue != nil {
            let existingIndex = self.array.indexOf(key)

            if existingIndex < index {
                adjustedIndex -= 1
            }

            self.array.removeAtIndex(existingIndex!)

        }

        self.array.insert(key, atIndex: adjustedIndex)
        self.dictionary[key] = value

        return existingValue

    }

這裏引入了幾個新東西,讓我們分步瞭解下:
1.插入方法(_:forKey:atIndex) 插入一個新的對象,這個方法需要三個參數:一個特定的座標值index和一個鍵值對,這裏有個關鍵詞mutating你可能沒有見過。

結構默認是不可修改的,也就是說你通常沒法在一個實例方法中修改結構裏的成員變量。因爲這個限制非常不方便,所以你可以添加關鍵字mutating告訴編譯器該方法可以改變結構的狀態。這有利於編譯器處理結構的複製問題(什麼時候複製,什麼時候重新寫入),也有利於API文檔的開發。

2.你通過類型key找到字典對應的值。如果這個key下面有值則返回對應的index。這個方法模擬的是字典更新值的方法,因此需要通過這個key來保存值。

3.如果這個key對應的值存在,則繼續用key來獲取在數組中的座標

4.如果對應值在插入索引值之前就存在了,你需要刪除掉數組中現有的值用於保存新的索引值。

5.適當的更新數組和字典的內容

6.最後,返回存在的值。因爲對應的key下面可能沒有值,所以返回的是一個可選類型的值

現在你可以向字典中添加內容了,那刪除可以嗎?

在OrderedDictionary結構定義中添加如下函數:

// 1
mutating func removeAtIndex(index: Int) -> (KeyType, ValueType) {
// 2
precondition(index < self.array.count, “Index超出邊界”)// 3
let key = self.array.removeAtIndex(index) // 4
let value = self.dictionary.removeValueForKey(key)
// 5
return (key, value)
}

又該分步講解了:
1.再一次的,因爲這個函數要修改結構的內部狀態,所以用mutating聲明這個函數。取名和數組中刪除的名字一樣叫removeAtIndex。適當考慮使用系統api的方法名,可以讓開發人員更易上手。

2.首先,你先要檢查下index有沒有超過數組的邊界。嘗試刪除越界的元素會在運行時發生錯誤,所以一定要在使用前早點檢查下。在Object-C中通常使用斷言assertions.斷言在Swift中同樣有效。但前提是隻能在發佈應用前使用,因爲斷言失敗則會使程序崩潰終止。

3.接着你從數組中獲取到字典中需要的關鍵字key,同時刪除數組中的值。

4.然後,你從字典中刪除對應key下的值,同時也將值返回。字典可能包含有一個不存在對應key,所以removeValueForKey的返回值是可選的。在現在這個案例中,你知道字典一定包含了一個對應key的值。因爲上面的插入方法是你唯一給這個字典添加值的入口。所以亦可以直接用強解!,因爲你知道數組有值,則字典對弈的一定會有值。

5.最後,返回一個包含了key和值的元組。數組removeAtIndex和字典removeValueForKey的相似之處在於他們都返回了現有的值。

Accessing values - 訪問值

你現在可以往字典中寫入內容但卻無法從中讀取數據,這對於一個數據結構來說顯然是不合理的!你現在要做的就是添加一個方法用來從字典中讀取數據。

打開OrderedDictionary.swift並且將下面的代碼添加到結構定義中,就放在剛剛的數組和字典變量聲明的下邊:

var count: Int {
    return self.array.count 
}

這是一個用來計算有序字典的計算屬性。一個這樣的數據結構通常需要這樣的一個統計信息。字典的數量和數組的總數是匹配的,所以這個方法很好實現。

接着,你需要個方法去訪問字典的元素。在Swift中,你訪問一個字典通常使用下標語法,就和下面一樣。

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 self.array.indexOf(key) != nil
            {

            } else {

                self.array.append(key)
            }
            // 5
            self.dictionary[key] = newValue
        }
    }

下面來代碼分析:
1.這是添加下標語法的方法,可以使用下標關鍵字subscript而不是用函數或變量來實現。在這裏的參數代表着你希望在方括號中出現的類型。

2.下標語法中可以包含有setter和getter方法,就像計算屬性那樣。注意上面的(a)和(b)就是分別定義了閉包函數。

3.getter方法比較簡單:需要用給定的key來訪問字典的值。字典的下標返回的是一個可選類型的值,以便處理對應的key下面沒有值的情況。

4.setter比較複雜:首先,他先檢查有序字典中是否有這個對應的key,如果不存在,需要將這個key添加到數組中。將新的key添加到數組的結尾是有意義的,所以用append來往數組中添加內容。

5.最後,你用這個key往字典中添加新的內容,通過隱式傳遞一個叫newValue的變量賦值。

現在你可以像一般的字典一樣用索引來訪問這個有序字典了。但是隻能使用特定的key才能訪問字典,如何才能像數組那樣通過index值來訪問。對於一個有序字典來說,用index來訪問元素內容也是同樣重要的。

類和結構可以定義多個不同類型參數的下標。在結構的定義下面添加如下代碼:

subscript(index: Int) -> (KeyType, ValueType) { // 1 
get { // 2 
    precondition(index < self.array.count, "Index超出邊界值") 
// 3 
    let key = self.array[index] // 4 
    let value = self.dictionary[key]! 
// 5 
    return (key, value) 
    } 
} 

這和你前面添加的下標語法是十分類似的,除了傳入的參數類型是Int,其他的用法一樣。因爲這個參數會用來作爲數組的索引,檢查是否越界。這一次返回的是包含有key和value的元組,因爲這就是你OrderedDictionary給定的結構值。

下面是代碼詳解:
1.這個下標語法中只有getter方法,當然你也可以實現一個setter方法。首先需要檢查index的範圍是不是在數組的範圍內。

2.索引index必須在數組的邊界值內,使用前需要用注意是是否會訪問了數組邊界值外的內容。

3.用index從數組中找到對應的key。

4.用獲取到的key繼續從字典中獲取值value。再次需要注意的是,這裏用的是強解,因爲你知道在數組中的任何一個key在字典中都存在。

5.最後,返回一個包含有key和value的元組。

挑戰:在這個下標語法中實現setter方法。就像前面定義的下標語法一樣,添加一個setter的閉包函數。

提示:包含有key和value的叫newValue的元組可以用來設置爲一個索引。

提示:你可以使用下面這種語法從元組中獲取值:let(key,value) = newValue

此時,你可能想知道如果KeyType是Int會發生什麼。泛型的好處是允許你任何哈希的類型都可以作爲key,包括int。在這種情況下,下標語法如何知道是使用哪個下標方法呢?所以,我們需要給編譯器提供更多的信息以便能知道你的意圖。所以如果你想用返回一個鍵值對的元組,編譯器就能知道它該使用array-stype的哪個下標語法。

讓我們看看他是如何工作的。創建一個新的Playground文件,點擊File\New\File…, 選擇iOS\Source\Playground ,並命名爲ODPlayground。

將OrderedDictionary中的代碼全部複製粘貼進來。你必須這樣做,因爲悲催的是,Playground不能“看到”任何在應用程序模塊的代碼。現在在Playground的底部添加如下代碼

var dict = OrderedDictionary<Int, String>() 
dict.insert("dog", forKey: 1, atIndex: 0) 
dict.insert("cat", forKey: 2, atIndex: 1) 
print(dict.array.description+":"+dict.dictionary.description) 
var byIndex: (Int, String) = dict[0] 
    print(byIndex) 
var byKey: String? = dict[2] 
    print(byKey)

查看底部輸出控制檯可見:
這裏寫圖片描述

在這個例子中,因爲字典用的是Int類型的Key,所以編譯器會根據看到的類型變量來分配使用哪個下標方法。因爲byIndex是一個(Int,String)的元組,爲了匹配你定義的元組類型的變量,所以編譯器知道使用數組那個下標方法來進行返回。
當你嘗試刪除變量byIndex和byKey的類型定義時,你會看到編譯器提示錯誤,表示編譯器不知道該使用哪個下標方法。

技巧提示:對於類型推斷。編譯器必須要用一個明確的類型表達式來獲知類型。當有多個有着相同參數類型但不同返回類型的方法時,需要具體的告知調用哪一個。在Swift中添加的方法可能會在編譯的時候選擇錯方法,所以需要小心注意。

在playground中練習瞭解有序字典是如何工作的,嘗試下添加,刪除,更改鍵和值得類型。然後返回到應用程序繼續開發。現在你的數據結構可以讀和寫入了。是時候將他運用到應用中了。

Aside: Assertions & preconditions - 斷言和前置條件

我們在前一節中添加了幾個先決條件語句的代碼preconditions。用他來檢查上下文傳遞的參數是否有效。斷言和前置條件有很多不同的地方。

斷言和前置條件都是當你想使用條件之前檢查條件是否正確並繼續執行代碼。如果條件爲false,則停止程序運行並讓程序崩潰。

兩者的區別在於,斷言用在版本發佈前,而前置條件無所謂。斷言是爲了捕捉在開發過程中發現的bug,而前置條件是爲了當判斷的條件不爲真時拋出一個致命error的錯誤。

使用一個斷言測試的例子。在控制器中設置一個view的層次結構,如果一個方法依賴的另一個方法已經被執行,則被assert斷言,如下:

private func configureTableView() { 
    self.tableView = UITableView(frame: CGRectZero) 
    self.tableView.delegate = self      
    self.tableView.dataSource = self 
    self.view.addSubview(self.tableView) 
}

private func configureHeader() { 
    assert(self.tableView != nil)
  let headerView = UIView(frame: CGRectMake(0, 0, 320, 50)) 
    headerView.backgroundColor = UIColor.clearColor() 
    let label = UILabel(frame: CGRectZero) 
    label.text = "My Table" label.sizeToFit()
  label.frame = CGRectMake(0, 0, 
    label.bounds.size.width, 
    label.bounds.size.height) headerView.addSubview(label) 
self.tableView.tableHeaderView = headerView
 } 

在這個例子中,configureHeader()需要一個已經被創建出來的tableView,所以要在tableView上設置頭view時用斷言確保這個對象存在。這可以防止在控制器初始化時弄錯了調用這些方法的順序從而調用nil對象引起應用崩潰。

你要用斷言必須趕在開發過程中。當你的tableView的頭view一直沒出現先別急着抱怨是不是你的電腦有問題,你應該去檢查下你的斷言判斷是不是寫錯了。因爲你會在開發的時候找到這些bug,所以在發佈的版本中就不需要再檢查了,需要去掉斷言。很明顯你不希望僅僅是一個愚蠢的小錯誤就會讓你的應用程序崩潰不是。

有趣的是編譯器在發佈應用的時候,允許斷言假設的條件是正確的,考慮下下面的代碼:

func foo(value: Int) { 
    assert(value > 0)
  if value > 0 { 
    print("Yes, it's greater than zero”)
 } else { 
    print("Nope") 
    } 
} 

這可能看起來有點奇怪,但他能說明了上面的觀點。當你傳入一個0時,如果是在調試狀態,則斷言失敗,應用程序崩潰。然而在發佈狀態時編譯會發生什麼呢?編譯器一直允許函數打印輸出:Yes, it’s greater than zero。

編譯器斷言的值大於0,在發佈狀態時,編譯器總是假設斷言一直正確。爲了優化代碼,只要你喜歡,可以刪除下面的if語句,因爲他假定的值大於0.是不是很聰明,整潔。

正如上文所說,preconditions前置條件也做着和斷言一樣的事,但是可以在發佈狀態下使用。當你需要十分確定一個條件時你可以使用它。比如你想要讀取一個數組裏的值,所以你想要檢查獲取值的索引值是否有效,如下:

func fetchPeopleBetweenIndexes(start: Int, end: Int) -> [Person] { 
    precondition(start < end)
  precondition(start >= 0)
   precondition(end <= self.people.count) 
    return Array(self.people[start..<end]) 
} 

在這個例子中,前置條件被用來檢查函數的輸入值是否有效。start必須比end小,start必須大於等於0,end必須小於等於people數組裏的總量。

這些檢查的代碼是有用的,因爲無論是在調試階段還是在發佈階段,一旦你讀取一個越界的數組值的時候都會讓你的程序崩潰。但是用代碼讓程序崩潰能獲取到更多和錯誤相關的信息。

斷言和前置條件都可以把一個字符串作爲第二個參數,用來在你錯誤時輸出你添加的信息。

一般經驗來說,執行時使用斷言一般在開發過程中獲取到錯誤的提示。前置條件用來防止當繼續執行時會造成嚴重錯誤的情況,數據的破壞或其他什麼的。用前置條件也不錯,當你爲別人開發代碼使用,比如OrderedDictionary,你希望其他程序員在輸入無效的輸入時會讓程序崩潰。

Adding image search - 添加圖片搜索

是時候將你的注意力重新轉移回程序開發中了。打開MasterViewController.swift並在兩個@IBOutlets後面添加變量聲明:

var searches = OrderedDictionary<String, [Flickr.Photo]>() 

這個有序字典用來保存用戶添加到Flickr中的搜索詞。正如你看到的,String映射爲key,Flicker數組映射爲值。Photo是從FlickrApi返回的值。注意到當前的使用方式就和一般的字典一樣,尖括號裏的是key和value。而這兩個屬性變成有序字典的KeyType和ValueType來實現。

你可能想知道爲什麼類型Flickr.Phonto 有一個點在之間。因爲Photo是被定義在Flickr類裏面的類。在Swift中這樣的層級結構很有用,幫助你通過用名稱空間結構來保證類的命名足夠簡潔。在Flickr類中,你可以直接使用photo類中的Photo,因爲通過上下文告訴了編譯器這是什麼。

接着找到被叫做

tableView(_:numberOfRowsInSection:)的TableView

的數據源方法,並進行修改。

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int 
{
  return self.searches.count 
} 

這個方法現在用OrderedDictionary來告訴tableView有多少行。

接着找到數據源方法

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.首先你從tableView的列表中獲取到cell。你需要將他轉換爲UITableviewCell,從dequeueReusableCellWithIndentifier中返回的是任意的對象(在Object-C中叫id),並不是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)
            self.tableView.reloadData()
            // 5
        }
    }
  }

當你點擊搜索按鈕時這個方法被調用。說下這個方法做了些什麼:
1.你讓search bar註銷掉第一響應者的身份,隱藏掉鍵盤。

2.然後用search欄上現在的文本作爲變量的值。Flickr的搜索需要一個搜索詞,所以用一個閉包來執行搜索的成功或失敗的情況。這個閉包只有一個參數:一個error錯誤或者返回結果的枚舉。

3.在錯誤的情況下,不進行任何操作。當然如果你願意,你也可以在這裏添加一個提示的對話框,但是我們現在就先這樣吧,儘量保持代碼的簡潔。我們現在在這裏告訴Swift的編譯器,如果返回的是error,則什麼都不做。

4.如果搜索成功,搜索返回的結果關聯到SearchResult的枚舉result中。將結果添加到有序字典中,搜索詞作爲key,將此搜索詞放置於第一位。

5.最後,你需要刷新tableView,因爲你現在更新了數據不是。(@ο@) 哇~,你現在可以看到應用程序可以搜索圖片了。

運行程序,使用搜索功能,應該能看到下面這樣的情況:

這裏寫圖片描述

這裏有兩點需要注意。
1.如果訪問不到的話可能需要打開vpn。
2.Info.plist修改下本地化,將en改爲美國
這裏寫圖片描述

不斷的搜索,你會發現你最新的搜索詞在tableView的最頂部。但是現在點開還無法看到圖片,是時候來完善下了。


Show me the photos! - 讓我們來展示照片

打開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
        } 

    }
  }

使用相同的訪問方法從tableView的選中cell中獲取,因爲你不需要搜索詞key,所以元組只需要綁定一個局部變量即可。

編譯並運行,搜索一個詞,然後點擊一行內容,可以看到ui如下:
這裏寫圖片描述

提示一下,因爲這個代碼比較老了,所以還會提示無法安全訪問http,所以在plist文件添加運行訪問http
這裏寫圖片描述

隨着搜索的不斷增加,搜索列表的內容會越來越長,所以我們再添加一個刪除功能。

Deleting searches - 刪除搜索

打開MasterViewController.swift 並在viewWillAppear後面添加代碼:

self.navigationItem.leftBarButtonItem = self.editButtonItem() 

這可以在導航欄的左側添加一個編輯按鈕,接着在viewWillAppear後面添加代碼:

override func setEditing(editing: Bool, animated: Bool) { 
    super.setEditing(editing, animated: animated) 
    self.tableView.setEditing(editing, animated: animated) 
} 

允許tableView可以使用編輯功能。

最後,在tableView的代理中找到

tableView(_:commitEditingStyle:forRowAtIndexPath:)

並進行修改:

func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
    if editingStyle == .Delete {
        self.searches.removeAtIndex(indexPath.row)
        tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
    }

  }

因爲你只想要一個刪除功能,所以這個方法只檢查了刪除樣式。當用戶通過左滑點擊了出現的刪除按鈕後,則這個方法會刪除有序字典searchers的值並更新tableView。
這裏寫圖片描述

恭喜恭喜,你已經完成了本應用的開發了!你現在有了一個有序的字典,並且你可以在任何應用中都可以使用。最重要的是,這個有序結構是通用的,只要支持hashable,所以類型的值都可以做字典的key。

Generic functions and protocols - 泛型函數和協議

你已經見過了泛型的結構和類,你可能還不知道,但是你已經用過了泛型的函數。前面用過的幾次find()便是這樣。但因爲find()已經被廢棄了,我現在一直用的indexOf()來替代的。不過無所謂,寫出來理解下:
find()的定義:

func find<C: Collection where C.GeneratorType.Element: Equatable> (domain: C, value: C.GeneratorType.Element) -> C.IndexType? 

indexOf()的定義:

public func indexOf(element: Self.Generator.Element) -> Self.Index?

好了該講下find()了:
這是一個全局的函數,注意在他的方法名後面有一個尖括號(就像我們剛剛定義的泛型結構一樣)。讓他成爲一個泛型函數。這裏提供了方法內的參數類型(即被搜索的東西)。他也間接的給值定義了類型,泛型類型參數GeneratorType 類型。

簡而言之,GeneratorType是集合中用來搜索的值的類型。返回類型也是基於集合中搜索值的類型。

打開OrderedDictionary.swift 並找到(_:forKey:atIndex:)。原來的代碼截圖:
這裏寫圖片描述

可以注意到的是用的find()方法中並沒有指定類型。

這是因爲Swift再次自己推斷了類型。泛型可以通過查看第一個參數的類型來推斷這個類型(集合中是用來搜索的值)。這是泛型類型中的C類型。

但這個GeneratorType是個什麼協議的集合呢?這個協議結合還必須要符合SequenceType協議,這表明類和結構都可以被認爲是一個序列。序列是指什麼呢?就是像數組這樣的。定義的協議如下:

protocol SequenceType {

    typealias GeneratorType : Generator 
    func generate() -> GeneratorType 
} 

這表示任何SequenceType必須有個類型別名叫GeneratorType,且本身是Generator類型。他還必須有個一用來返回GeneratorType的函數。

實際背後的操作是:當需要迭代一個序列時,他在序列中通過generate()生成一個generator。這個generator有個叫next()的方法讓他獲取到序列中的下一個對象。generator從序列的開頭處開始,所以你可以用他來從到到尾的迭代序列。

提示:Generators在處理很大或者計算開銷很高的集合時非常有用。因爲他允許你只生成你需要的內容,而不是將所有的集合內容都編譯一遍。

那麼OrderedDictionary呢?你明顯也會考慮他爲一個序列(雖然他是個字典,有着key和value)。但他是個有序的字典,所以應該也讓他符合SequenceType讓你可以輕鬆的進行遍歷。

打開ODPlayground.playground,這個playground是早前創建好的。在OrderedDictionary 結構定義的下邊添加代碼:

extension OrderedDictionary: SequenceType { // 1
    typealias GeneratorType = AnyGenerator<(KeyType, ValueType)>
    // 2
    func generate() -> AnyGenerator<(KeyType, ValueType)> { // 3
        var index = 0
        // 4
        return AnyGenerator { // 5
            if index < self.array.count {
                let key = self.array[index]
                index += 1
                return (key, self.dictionary[key]!)
            } else {
                return nil
            }
        }
    }
}

這個擴展定義了OrderedDictionary必須遵循SequenceType協議。下面是處理情況:
1.GeneratorType作爲SequenceType協議的別名,必須符合Generator。你也可以自己編寫一個Generator,但Swift有個標準的稱爲GeneratorOf的結構,每次都可以執行閉包裏的next()。返回的對象是一個泛型的結構。因此在OrderedDictionary中,你設置keyType和ValueType的元組作爲泛型的返回值。

2.然後實現generate(),最後的實現需要符合SequenceType。

3.如上面提到的,GeneratorOf的閉包函數中每次都會執行next().在實現的地帶中,你可以保存當前的index作爲index的變量,因爲他是從0開始的。

4.然後你生成並返回一個GeneratorOf。雖然他看起來不太像,但是實際上他調用了GeneratorOf的初始化。只需要一個參數,閉包函數就能每次都調用next()方法了。

5.在generator閉包函數中,你需要檢查索引是否在數組邊界值內。如果是,則返回key和value在增量索引下的值。如果超出了數組的邊界值(即代表迭代結束),返回一個nil。

讓我們來看看如何使用這個遍歷功能,在playground的最底部添加代碼:

for (key, value) in dict { 
    print("\(key) => \(value)") 
} 

可以看到控制檯輸出

1 => dog
2 => cat

就如你所希望的,命令執行正確,每一個鍵值對的關係也是正確的。

通過實現SequenceType,你讓OrderedDictionary實現了枚舉遍歷的功能。

表急,這裏還有點東東!刪除在OrderedDictionary SequenceType擴展協議中的別名聲明。你會發現playground仍然能夠正確的執行。這不是很奇怪嗎?這依然是Swift的類型推斷功能。他能夠推斷出別名應該是GeneratorOf<(KeyType, ValueType)>,因爲他返回的就是generate()的類型~~。

如果你仔細想想,這個SequenceType 協議其實是一個泛型協議。類型別名GeneratorType和泛型類,結構或函數中的泛型參數非常的像。但是泛型不用用尖括號語法來表示協議,不像java和C#那樣。

這主要是語法的原因。協議的定義用來是接口類和結構都遵循。在某種程度上,他們也算是一種“泛型”!相反,Swift分離開了你關心的協議定義和類,結構類型以及其他的事物。

這部分是GeneratorOf存在的原因:他在一定程度上提升了Generator協議。當然,這只是實現了一個這樣的Generator,以後你可能經常會使用到。

Where to go from here? - 接着幹什麼

在本章中,你已經瞭解了怎樣在一個app中寫一個泛型的結構。你可以用同樣的方法寫一個泛型類。根據你其他應用存儲數據的需要,你可以重新定製你的有序字典。

你還在本章瞭解了下下標語法subscripts的用法。下標語法對集合來說非常有用,可以讓你輕鬆的訪問元素。在任何類和結構中用下標語法進行訪問都是非常直觀的。只要能實現,有什麼理由不用下標語法來實現呢!

在Swift的代碼繼續學習中,想想如何讓泛型和系統類型讓開發更容易些。當用泛型時可以讓你兼顧類型的安全與重用性。想象下你如何在項目中使用泛型,你就會發現爲什麼相比於Object-C,Swift的類型安全語言更好了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章