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
。當然你可以使用任意的占位符,例如Placeholder
或SomeType
,不過推薦使用T
,因為它是一個約定俗稱的規(guī)定。
占位符T
并不是表示這個方法的參數接收T
類型的入參,相反,T
會被替換為一個具體的類型,而這個類型是在方法被調用的時候才能決定。如果我們調用printElementInArray方法,并傳入一個字符串數組,那么T
被指定為String類型。基于這樣的機制,我們可以使用這一個方法來遍歷并打印三個數組。
泛型定義的函數可以接受任意的類型作為入參,這無疑讓我們編寫的代碼更具靈活性。事實上,在定義函數時我們可以指定多個泛型類型。例如下面的代碼,
func someFunction<T, U>(a: T, b: U) {}
這里,我們使用了T
和U
兩個泛型作為占位符來定義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代碼。
參考鏈接
- https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Generics.html
- http://swiftyeti.com/generics/
- https://milen.me/writings/swift-generic-protocols/
公眾號
微信掃描下方圖片,歡迎關注本人公眾號foolishlion,咱們來談技術談人生,因為這又不要錢,