Swift - 進階之泛型編程

generic-in-swift.jpg

Swift語言有很多強大的特性,泛型編程(generic programming)就是其中之一,我們也可以將其簡稱為泛型(generic)。使用泛型編碼,可以編寫出更加靈活(flexible)、可復用(reuseable)的函數和類型,與此同時它還保證了swift的類型安全(type safety)。

痛點 -- The Problem

OC的集合類型在運行時可以包含任意類型的對象,這一方面讓它具有很多的靈活性,但同時也意味著OC缺少安全性。當使用OC提供的API時,我們不能保證一個特定的集合返回文檔中所指示(indicated)的類型。Swift使用類型集合(typed collections)解決了OC存在的安全問題,但是這也造成了很多重復冗余的代碼。讓我們看看下面列舉的一些例子,

let stringArray = ["1", "2", "3", "4"]
let intArray = [1,2,3,4]
let doubleArray = [1.1, 1.2, 1.3, 1.4]

上面,我們定義了三個類型集合,分別是字符串數組stringArray, 整形數組intArray以及雙精度浮點型數組doubleArray,此時如果想遍歷這3個數組,并打印出數組中的每一個元素,可以針對這三個不同的類型數組分別處理,如下代碼所示,


func printStringFromArray(a: [String]) {
    for s in a {
        println(s)
    }
}

func printIntFromArray(a: [Int]) {
    for i in a {
        println(i)
    }
}

func printDoubleFromArray(a: [Double]) {
    for d in a {
        println(d)
    }
}

這樣做可以解決我們小小的需求,但是它并不夠理想。上述3個函數的函數體可以說是完全相同的,唯一的區(qū)別在于它指定類型的函數簽名,也就是3個函數的參數不一樣。那么,我們可以將3個函數抽象、整合,并用下面的方式重寫,

func printElementFromArray(a: [Any]) {
    
}

上面的Any類型在swift中表示所有的類型,包括結構體(struct), 函數(function), 類(class), 枚舉(enum)等。我們可以使用函數printelementFromArray來遍歷數組并打印每一個數組元素。但是這同樣不夠理想,因為當我們使用這個參數是[Any]類型的方法時,則失去了類型安全。為什么這么說呢?因為使用[Any]類型的數組時,我們并不能保證[Any]只包含字符串數據或整形數據。

讓我們看一個使用[Any]數組失效的例子 -- 交換兩個變量的值,可能我們都寫過交換兩個字符串的值,如下代碼所示,

func swapTwoStrings(inout1 a: String, inout2 b: String) {
    let tempA = a
    a = b
    b = tempA
}


當然這里不是討論怎樣交換兩個字符串,而是要支持任意類型。現在我們對其作簡單擴展,讓它可以處理任意類型,那么我們使用Any類型來處理這樣的需求,如下代碼所示,

func swapTwo(inout1 a: Any, inout2 b: Any) {
    let tempA = a
    a = b
    b = tempA
}

代碼看起來很棒,如果我們調用該方法來交換兩個字符串的值,這個方法可以通過編譯,并且運行也可以得到我們想要的結果。看看下面的代碼,調用該方法來交換兩個字符串,

var firstString = "someString"
var secondString = "anotherString"
swapTwo(inout1: firstString, inout2: secondString)

然而,它還是存在一個隱藏的問題,如果調用該方法時傳遞的兩個參數,一個是String類型,一個是Int類型,那么就會出現運行時崩潰,如下代碼所示,

var firstString = "someString"
var someInt = 1
swapTwo(inout1: firstString, inout2: someInt)

上面的代碼可以通過編譯(compile),因為String和Int參數對于swapTwo函數的Any類型參數來說是合法的入參。但是在運行時則會導致崩潰,因為firstString是一個String類型的變量,不能將其賦值給Int類型的對象。所以說,使用Any時,我們并不能保證類型安全。

泛型 -- Generics

上述的問題讓我們很困擾,不過不必擔心,我們可以使用泛型來重寫swapTwo函數,它可以完美地解決類型安全的問題。

先看一個例子,使用泛型來輸出數組中的值,如下代碼所示,


func printElementInArray<T>(a: [T]) {
    for element in a {
        println(element)
    }
}

使用泛型定義的函數與之前使用Any定義的函數最主要的區(qū)別在于泛型函數的參數省略了特定的參數類型,而使用T來替代。那么,T又是什么呢?

泛型函數使用占位符(placeholder)名稱來代替一個具體的類型,比如String, Int或Double。上面代碼定義的泛型函數中,占位符是T。當然你可以使用任意的占位符,例如PlaceholderSomeType,不過推薦使用T,因為它是一個約定俗稱的規(guī)定。

占位符T并不是表示這個方法的參數接收T類型的入參,相反,T會被替換為一個具體的類型,而這個類型是在方法被調用的時候才能決定。如果我們調用printElementInArray方法,并傳入一個字符串數組,那么T被指定為String類型。基于這樣的機制,我們可以使用這一個方法來遍歷并打印三個數組。

泛型定義的函數可以接受任意的類型作為入參,這無疑讓我們編寫的代碼更具靈活性。事實上,在定義函數時我們可以指定多個泛型類型。例如下面的代碼,

func someFunction<T, U>(a: T, b: U) {}

這里,我們使用了TU兩個泛型作為占位符來定義someFunction,此時參數a必定是T類型的,b必定是U類型的。如果我們在調用的時候傳入的參數與指定的泛型不一致時,則編譯器會報錯,如下代碼所示,

someFunction<String, Int>(1, "Test")

之所以報錯是因為指定的泛型<String, Int>與入參不一致。

本文的代碼是swift 2.3之前的語法,在swift 3.0中,調用someFunction不可以指定<String, Int>,讀者可以用swift 2.3之前的版本驗證。

如果輸入參數不符合泛型的類型,則不能編譯通過,那么基于這個原則,我們來考慮使用泛型來重新實現swapTwo函數,如下代碼所示,

func swapTwo<U>(inout1 a: U, inout2 a: U) {
  let tempA = a
  a = b
  b = tempA
}

這時,如果在調用swapTwo函數時,例如swatTow("124", 123),沒有傳遞正確的參數,則編譯器會報錯。

其實,我們常用的swift集合操作map, filter, reduce和flatMap實現原理都是基于泛型編程,例如我們可以使用map操作快速對整形數組中每個元素求平方,如下代碼所示,

let intArray = [1, 3, 4, 2, 8, 5]
let newIntArray = intArray.map { $0 * $0 }

如果讓你來實現map函數,你會怎樣處理呢,要保證map可以作用于包含任意類型的集合,那么首選當然是泛型了,可以參考下面的代碼,它提供了map簡單的實現方式,

extension Array {
  func map<U>(transform: Element -> U) - [U] {
    var result: [U] = []
    // 分配存儲空間
    result.reserveCapacity(self.count)
    for e in self {
      result.append(transform(e))
    }
    return result
  }
}

在這里,Element是數組中包含的元素類型的占位符,U是元素轉換之后的類型的占位符。map函數本身并不關心Element和U究竟是什么,他們可以是任意類型,它們的實際類型會留給調用者來決定。

能夠在方法調用的時候指定泛型的類型,無疑是一個巨大的優(yōu)勢,但有時候我們不想要任意或所有的類型,那么我們就需要學習了解泛型的另一個特性 -- 類型約束。

思考:鑒于map函數的實現方式,讀者可以嘗試實現filter的功能。

類型約束 -- Type Constraints

關于泛型編碼,我們可以更進一步,現在來探討一下泛型約束(constraints)。

第一種泛型約束 - 泛型繼承自指定的父類

首先,關于泛型約束,我們可以指定泛型類型繼承自一個指定的父類(superclass)。比如現在我們有一個數字圖書館(digital library)類型的應用,在這個數字圖書館應用中,我們有書籍(books),電影(movies),tv秀,音樂(songs)和博客(podcasts)等資源,這些資源都繼承自一個父類Media。

我們可以定義一個泛型排序函數,這個函數允許我們使用多種過濾條件對圖書館里的資源進行排序。因為這個數字圖書館應用程序除了Media類型的資源,還包含其他的資源,為了簡化模型,我們假設定義的排序函數只接受Media及其子類作為函數參數,如下代碼所示,是我們實現排序函數的一種方式,

func sortEntertainment<T: Media>(collection: [T]) {
    // todo
    // sort by specified filters
}

這個泛型函數sortEntertainment,接收一個[T]類型的集合對象作為參數,并對該集合按照特定的方式進行排序。當我們調用這個函數,并傳入一個集合作為參數時,這個指定的T類型必須繼承自Media類型,也就是說作為參數傳入的集合只能包含Media或Media子類的對象。

所以如果我們傳入了[Media]和[Book]集合,sortEntertainment函數可以有效運行,如下代碼,

let medias: [Media] = [Media]()
sortEntertainment(collections: medias)

let bookShelf: [Book] = [Book]()
func sortMedia(collection:bookShelf)

但如果傳入[Shirt]集合,編譯器就會報錯,如下代碼,

let clothesRack: [Shirt] = [Shirt]()
func sortMedia(collection: clothesRack) // Error, insert as! [Media] 

第二種泛型約束 - 泛型實現指定的接口

其次,關于泛型約束,我們可以要求泛型實現某個特定的接口(protocol)。

一個相關的例子就是swift中的Set,Set就像數組(Array)和字典(Dictionary)一樣,它存儲了無序且不重復的元素。在swift 1.0中并沒有實現標準的Set功能,所以開發(fā)者為了使用Set的功能,就得自己來實現Set的功能。

例如,這里有一個來自于NSHipster站點供稿作者的實現,如下代碼所示,

struct Set<T> {
    typealias Element = T
    private var contents: [Element: Bool]

    init() {
        self.contents = [Element: Bool]()
    }

    // implementation
}


上面代碼中,定義Set結構時,使用swfit原生的字典來進行數據存儲,該字典命名為contents。將傳入的類型作為字典contents的鍵key,并使用Bool值true來標志Set中已經包含了某個特定的值,這是就不能將該值添加到Set中,這樣就保證了Set集合中的元素不重復。

在swfit中,將一個類型作為字典的鍵key有一些限定條件,比如該類型的對象可以轉換為hash。為了實現這個需求,我們可以再構造自定義Set的時候指定傳入的類型實現了Hashable接口。那么對代碼做如下調整即可,

// 指定T實現Hashable協(xié)議
struct Set<T: Hashable> {
    typealias Element = T
    private var contents: [Element: Bool]

    init() {
        self.contents = [Element: Bool]()
    }

    // implementation
}


這樣,我們就可以限定泛型函數的入參類型,它必須實現Hashable接口。

泛型類型 -- Generic Types

上面討論的都是泛型函數,然而我們還能圍繞泛型做更多事情,因為swift同樣允許我們定義泛型類型(generic type),盡管在文章中并沒有明確說出泛型類型這樣的概念,但我們確實已經定義了一個泛型類型 -- 自定義的Set就是一個泛型類型的結構體。

泛型類型包括類(class),枚舉(enumeration)和結構體(struct),它們可以操作任意類型,就像swift中Array和Dictionary等集合類型所能做的功能一樣。

現在,我們把文章所提及的知識點總結一下,使用一個簡單的返利,看看使用泛型我們可以做什么事情。我們有一個包含字符串的數組,并定義一個函數來返回一個隨機的字符串,如下代碼所示,

struct FactBook {

    let facts = ["aFact", "anotherFact", "blah blah"]

    static func getRandomFact() -> String {
        let randomIndex = Int(arc4random_uniform(UInt32(facts.count)))
        return facts[randomIndex]
    }
}


如果我們想定義一個通用的結構體,可以用它返回一個隨機的值,不論這個值是String類型還是Int類型。使用泛型可以輕易地解決這樣簡單的需求,

首先我們定義一個泛型結構體RandomContainer,它基于通用的類型T,如下代碼,

struct RandomContainer<T> {
    
}

接著,為它添加一個[T]類型的屬性,用來存放該在初始化結構體時候傳入的數據,如下代碼所示,

struct RandomContainer<T> { 
    let items: [T]

    init(items: [T]) {
        self.items = items
    }
}

最后,我們定義一個隨機函數來返回一個T類型的值,如下代碼所示,

struct RandomContainer<T> {
    let items: [T]

    init(items: [T]) {
        self.items = items
    }

    func getRandom() -> T {
        let randomIndex = Int(arc4random_uniform(UInt32(items.count)))
        return items[randomIndex]
    }   
}

我們來創(chuàng)建三個數組,它們分別是包含String和Int的數組,并將該數組作為RandomContainer結構體初始化的入參傳入,并隨機返回一個數組中的值,如下代碼所示,

let randomIntegers = RandomContainer(items: [1,2,3,4,5,6,7])
randomIntegers.getRandom()

let randomStrings = RandomContainer(items: ["a", "b", "c", "d", "e"])
randomStrings.getRandom()


總結

盡管這是一篇簡短的教程,但它應該能夠給讀者一個不錯的概述,那就是泛型是怎樣幫助減少冗余代碼、編寫更具靈活性的函數和類型的。

泛型并不是swift獨創(chuàng)性的概念,在Java等其他語言中也有泛型的概念,但由于在OC中沒有泛型,所以對于大多數iOS平臺的開發(fā)者來說泛型是有點陌生的概念,現在筆者在使用swift編程,也感覺到使用泛型編碼所帶來的很實在的好處。希望讀者也能習慣使用泛型,寫出更好更簡潔的swift代碼。

參考鏈接

公眾號

微信掃描下方圖片,歡迎關注本人公眾號foolishlion,咱們來談技術談人生,因為這又不要錢,

foolishlion.jpg
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容