【Swift 3.1】23 - 泛型 (Generics)
自從蘋果2014年發(fā)布Swift,到現(xiàn)在已經(jīng)兩年多了,而Swift也來到了3.1版本。去年利用工作之余,共花了兩個(gè)多月的時(shí)間把官方的Swift編程指南看完?,F(xiàn)在整理一下筆記,回顧一下以前的知識(shí)。有需要的同學(xué)可以去看官方文檔>>。
泛型代碼能讓我們編寫靈活、可重復(fù)使用的方法和類型??梢员苊饩帉懼貜?fù)代碼,并以清晰和抽象的方法表達(dá)其目的。
泛型是Swift非常強(qiáng)大的一個(gè)特性,并且很多Swift標(biāo)準(zhǔn)庫都是用泛型編寫的。實(shí)際上我們已經(jīng)使用過泛型,例如Swift的Array和Dictionary都是泛型集合。
泛型能解決的問題 (The Problem That Generics Solve)
下面這個(gè)方法只能交換兩個(gè)Int值,使用in-out參數(shù)來交換a和b:
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
上面這個(gè)方法只能交換Int,如果我想交換兩個(gè)String類型的值或者兩個(gè)Double類型的值,我們又得寫兩個(gè)方法swapTwoStrings(_:_:)和swapTwoDoubles(_:_:):
func swapTwoStrings(_ a: inout String, _ b: inout String) {
let temporaryA = a
a = b
b = temporaryA
}
func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
let temporaryA = a
a = b
b = temporaryA
}
使用泛型代碼讓我們用一個(gè)方法就能解決上面的問題。
泛型方法 (Generic Functions)
上面的三個(gè)方法可以用下面的額泛型方法代替:
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}
這個(gè)通用的方法使用T作為類型名稱,而不是Int、String或者Double。T沒有說明必須是什么類型,但是a和b的類型必須是一樣的。
方法名后面還有一個(gè)<T>,告訴Swift只是一個(gè)占位類型,不是實(shí)際類型。
swapTwoValues(_:_:)方法可以像之前的swapTwoInts一樣調(diào)用,只要兩個(gè)用于交換的值類型是一樣的。
用通用的方法交換兩個(gè)String類型的值或者兩個(gè)Double類型的值:
var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt is now 107, and anotherInt is now 3
var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString is now "world", and anotherString is now "hello"
注意:其實(shí)Swift的標(biāo)準(zhǔn)庫中已經(jīng)有一個(gè)功能與swapTwoValues(_:_:)一樣的方法swap。如果我們需要交換兩個(gè)值,直接使用swap即可,無需自己另外實(shí)現(xiàn)這個(gè)功能。
類型參數(shù) (Type Parameters)
上面的泛型方法swapTwoValues(_:_:),占位類型T其實(shí)是一個(gè)類型參數(shù)。類型參數(shù)指定并命名占位類型,緊跟在方法名后面,用<T>表示。
只要定義好了類型參數(shù),我們就可以用來定義方法的參數(shù)類型(例如swapTwoValues(_:_:)的參數(shù)a和b的類型),或者作為方法的返回類型。
我們可以定義多個(gè)類型參數(shù),寫法是:<T1, T2, ...>。
類型參數(shù)命名 (Type Parameters)
在很多情況下,類型參數(shù)有描述性的名稱,例如Dictionary<Key, Value>中的Key和Value,Array<Element>中的Element,這些名字都能告訴讀者類型參數(shù)和泛型的關(guān)系。但是,在沒有任何意義的情況下,我們一般把類型參數(shù)名命名為T、U和V。
注意:需要使用駱駝命名法給類型參數(shù)命名,例如T和MyTypeParameter,以表示他們是一個(gè)類型的占位。
泛型類型 (Generic Types)
除了泛型方法,Swift還可以定義泛型類型,例如Array和Dictionary。
這一部分將演示如何寫一個(gè)泛型集合Stack。一個(gè)棧是一個(gè)有序的值的集合,類似一個(gè)數(shù)組,但是比數(shù)組有更嚴(yán)格的運(yùn)算。 數(shù)組可以在特定的位置移除或插入元素。而棧只允許在集合的最后添加元素,也只允許從最后面移除元素。
注意:棧的概念就被用于UINavigationController管理控制器。使用pushViewController(_:animated:)來添加新的控制器到棧中,用popViewControllerAnimated(_:)移除控制器。棧非常適合用來管理“后進(jìn)先去”的集合。
下圖演示了棧的push/pop:

- 目前棧中有三個(gè)值
- 第四個(gè)值被push到棧頂
- 棧中有四個(gè)值
- 第四個(gè)值被移除(popped)
- 移除第四個(gè)值后,棧中剩下三個(gè)值
下面是一個(gè)不通用的IntStack:
struct IntStack {
var items = [Int]()
mutating fuc push(_ item: Int) {
items.append(item)
}
mutating fuc pop() -> Int{
return items.removeLast()
}
}
下面是一個(gè)通用版本的Stack:
struct Stack<Element> {
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
Element定義了一個(gè)占位名字,并且在三個(gè)地方被用到:
- 創(chuàng)建一個(gè)空數(shù)組
items,其元素類型為Element - 作為
push(_:)方法的參數(shù)類型 - 作為
pop()方法的返回值
因?yàn)樗且粋€(gè)通用類型,所以Stack可以用來創(chuàng)建任意有效類型的棧:
var stackOfThings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// the stack now contains 4 strings
下面是stackOfThings的push過程:

移除棧頂?shù)闹?code>"cuatro":
let fromTheTop = stackOfStrings.pop()
// fromTheTop is equal to "cuatro", and the stack now contains 3 strings
移除過程如下:

擴(kuò)展泛型類型 (Extending a Generic Type)
在擴(kuò)展泛型類型是,我們不必提供類型參數(shù),可以直接使用已有泛型類型的類型參數(shù)。
下面是對(duì)Stack的擴(kuò)展:
extension Stack {
va topItem: Element? {
return items.isEnpty ? nil : items[items.count - 1]
}
}
訪問topItem屬性:
if let topItem = stackOfStrings.topItem {
print("The top item on the stack is \(topItem).")
}
// Prints "The top item on the stack is tres."
泛型類型的擴(kuò)展也可以指定一些限定條件,滿足這些條件才可以訪問擴(kuò)展里面的成員,下面會(huì)講到。
類型約束 (Type Constraints)
swapTwoValues(_:_:)和Stack可以用于任何類型。然而,在某些類型上添加一些類型約束是非常有用的。類型約束指定一個(gè)類型參數(shù)必須繼承于特定的類或者遵循特定的協(xié)議。
例如,Swift的Dictionary限定了鍵的類型。字典的鍵必須是hashable的,也就是說,必須提供一個(gè)方法保證它自己是唯一的,然后字典才能根據(jù)這個(gè)鍵去查找對(duì)應(yīng)的值。Swift的基本類型默認(rèn)都是hashable的,例如String、Int、Double和Bool。
類型約束語法 (Type Constraint Syntax)
語法如下:
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// function body goes here
}
上面的語法要求T必須是SomeClass的子類,U必須遵循SomeProtocol協(xié)議。
類型約束實(shí)踐 (Type Constraint in Action)
下面是一個(gè)不通用的findIndex(ofString:in:),用于查找指定字符串在字符串?dāng)?shù)組的索引:
func findIndex(ofString valueToFind: String, in array: [String]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
if let foundIndex = findIndex(ofString: "llama", in: strings) {
print("The index of llama is \(foundIndex)")
}
// Prints "The index of llama is 2"
我們可以把上面的方法改為通用形式:
func findIndex<T>(of valueToFind: T in array: [T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return value
}
}
return nil
}
其實(shí)這個(gè)方法不能編譯通過,因?yàn)?code>value == valueToFind。不是所有Swift類型的都可使用==進(jìn)行比較。
Swift定義了Equatable協(xié)議,要求所有遵循這個(gè)協(xié)議的類型必須實(shí)現(xiàn)==和!=運(yùn)算符。Swift的標(biāo)準(zhǔn)類型都遵循了Equatable協(xié)議。
所以上面的通用方法改寫為:
func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
T: Equatable類型參數(shù)意味著任意類型T必須遵循Equatalbe協(xié)議。
例如:
let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])
// doubleIndex is an optional Int with no value, because 9.3 is not in the array
let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"])
// stringIndex is an optional Int containing a value of 2
關(guān)聯(lián)類型 (Associated Types)
當(dāng)定義協(xié)議時(shí),定義一個(gè)或多個(gè)關(guān)聯(lián)類型有時(shí)候是非常有用的。關(guān)聯(lián)類型提供了一個(gè)類型名字,然后這個(gè)類型可以在協(xié)議中使用。使用associatedtype關(guān)鍵字來定義關(guān)聯(lián)類型。
關(guān)聯(lián)類型實(shí)踐 (Associated Types in Action)
下面是一個(gè)例子:
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
這個(gè)容器協(xié)議中沒有具體說明能存儲(chǔ)那個(gè)類型的值。遵循這個(gè)協(xié)議的類型必須寫清楚能存儲(chǔ)的值類型。
之前那個(gè)IntStack類型遵循Container協(xié)議:
struct Instack: Container {
// original Instack implementation
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
// conformance to the Container protocol
typealias Item = Int
mutating func append(_ item: Int) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Int {
return items[i]
}
}
IntStack指定了Item是一個(gè)Int類型。typealias Item = Int把抽象的Item轉(zhuǎn)變?yōu)榫唧w的Int。
因?yàn)镾wift有類型推斷功能,所以我們可以不用寫typealias Item = Int,因?yàn)?code>IntStack實(shí)現(xiàn)了Container協(xié)議的所有要求,Swift能推斷出需要用的Item的具體類型。
之前那個(gè)Stack類型遵循Container協(xié)議:
struct Stack<Element>: Container {
// original Stack<Element> implementation
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
// conformance to the Container protocol
mutating func append(_ item: Element) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Element {
return items[i]
}
}
通過擴(kuò)展類型來指定關(guān)聯(lián)類型 (Extending an Existing Type to Specifying an Associated Type)
我們可以使用擴(kuò)展來遵循某一個(gè)協(xié)議,當(dāng)然這個(gè)協(xié)議也可以是由關(guān)聯(lián)類型的協(xié)議。
Swift的Array已經(jīng)提供了append(_:)方法、count屬性和下標(biāo),那么就滿足了Container協(xié)議的要求。所以我們可以是下面這種形式來聲明Array遵循Container協(xié)議:
extension Array: Container {}
我們就可以把Array當(dāng)做是一個(gè)Container。
泛型的Where語句 (Generic Where Clauses)
泛型的where語句可以讓我們要求關(guān)聯(lián)類型遵循一個(gè)特定的協(xié)議,或者關(guān)聯(lián)類型和類型參數(shù)必須相同。
例如下面這個(gè)例子:
func allItemsMatch<C1: Container, C2: Container> (_ someContainer: C1, _ anotherContainer: C2) -> Bool where C1.Item == C2.Item, C1.Item: Equatalbe {
// Check that both containers contain the same number of items.
if someContainer.count != anotherContainer.count { return false }
// Check each pair of items to see if they are equivalent.
for i in 0..<someContainer.count {
if someContainer[i] != anotherContainer[i] { return false }
}
// All items match, so return true.
return true
}
allItemsMatch(_:_:)方法的實(shí)際使用:
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
var arrayOfStrings = ["uno", "dos", "tres"]
if allItemsMatch(stackOfStrings, arrayOfStrings) {
print("All items match.")
} else {
print("Not all items match.")
}
// Prints "All items match."
雖然Array和Stack是不同的類型,但是他們都遵循Container協(xié)議,并且都包含相同類型的值。
有泛型Where語句的擴(kuò)展 (Extensions with a Generic Where Clause)
例如下面這個(gè)例子:
extension Stack where Element: Equatable {
func isTop(_ item: Element) -> Bool {
guard let topItem = items.last else {
return false
}
return topItem == item
}
}
如果沒有泛型where語句,將會(huì)出現(xiàn)一個(gè)問題:isTop(_:)方法使用了==運(yùn)算符,但是Stack沒有要求它的元素是可以比較的,所以使用==將會(huì)編譯錯(cuò)誤。
下面是isTop(_:)方法的使用:
if stackOfStrings.isTop("tres") {
print("Top element is tres.")
} else {
print("Top element is something else.")
}
// Prints "Top element is tres."
下面是使用where擴(kuò)展Container:
extension Container where Item: Equatalbe {
func startsWith(_ item: Item) -> Bool {
return count >= 1 && self[0] == item
}
}
startsWith(_:)適用于遵循了Container協(xié)議的類型:
if [9, 9, 9].startsWith(42) {
print("Starts with 42.")
} else {
print("Starts with something else.")
}
// Prints "Starts with something else."
上面對(duì)Container的擴(kuò)展要求Item遵循Equatable協(xié)議,我們還可以要求Item是一個(gè)具體的類型:
extension Container where Item == Double {
func average() -> Double {
var sum = 0.0
for index in 0..<count {
sum += self[index]
}
return sum / Double(count)
}
}
print([1260.0, 1200.0, 98.6, 37.0].average())
// Prints "648.9"
我們還可以在where語句中可以添加多個(gè)約束條件,條件之間用逗號(hào)隔開即可。
第二十三部分完。下個(gè)部分:【Swift 3.1】24 -訪問權(quán)限
如果有錯(cuò)誤的地方,歡迎指正!謝謝!