可選鏈(Optional Chaining)是一種可以請求和調用屬性、方法及子腳本的過程,它的自判斷性體現於請求或調用的目標當前可能爲空(nil
)。如果自判斷的目標有值,那麼調用就會成功;相反,如果選擇的目標爲空(nil
),則這種調用將返回空(nil
)。多次請求或調用可以被鏈接在一起形成一個鏈,如果任何一個節點爲空(nil
)將導致整個鏈失效。
擴展就是向一個已有的類、結構體或枚舉類型添加新功能(functionality)。這包括在沒有權限獲取原始源代碼的情況下擴展類型的能力(即逆向建模)。擴展和
Objective-C 中的分類(categories)類似。(不過與Objective-C不同的是,Swift 的擴展沒有名字。)
擴展語法(Extension Syntax)
聲明一個擴展使用關鍵字extension
:
extension SomeType {
// 加到SomeType的新功能寫到這裏
}
一個擴展可以擴展一個已有類型,使其能夠適配一個或多個協議(protocol)。當這種情況發生時,接口的名字應該完全按照類或結構體的名字的方式進行書寫:
extension SomeType: SomeProtocol, AnotherProctocol {
// 協議實現寫到這裏
}
方法(Methods)
擴展可以向已有類型添加新的實例方法和類型方法。下面的例子向Int
類型添加一個名爲repetitions
的新實例方法:
extension Int {
func repetitions(task: () -> ()) {
for i in 0..self {
task()
}
}
}
這個repetitions
方法使用了一個()
-> ()
類型的單參數(single argument),表明函數沒有參數而且沒有返回值。
定義該擴展之後,你就可以對任意整數調用repetitions
方法,實現的功能則是多次執行某任務:
泛型代碼可以讓你寫出根據自我需求定義、適用於任何類型的,靈活且可重用的函數和類型。它的可以讓你避免重複的代碼,用一種清晰和抽象的方式來表達代碼的意圖。
泛型是 Swift 強大特徵中的其中一個,許多 Swift 標準庫是通過泛型代碼構建出來的。事實上,泛型的使用貫穿了整本語言手冊,只是你沒有發現而已。例如,Swift 的數組和字典類型都是泛型集。你可以創建一個Int
數組,也可創建一個String
數組,或者甚至於可以是任何其他
Swift 的類型數據數組。同樣的,你也可以創建存儲任何指定類型的字典(dictionary),而且這些類型可以是沒有限制的。
泛型函數
泛型函數
可以工作於任何類型,這裏是一個上面swapTwoInts
函數的泛型版本,用於交換兩個值:
func swapTwoValues<T>(inout a: T, inout b: T) {
let temporaryA = a
a = b
b = temporaryA
}
swapTwoValues
函數主體和swapTwoInts
函數是一樣的,它只在第一行稍微有那麼一點點不同於swapTwoInts
,如下所示:
func swapTwoInts(inout a: Int, inout b: Int)
func swapTwoValues<T>(inout a: T, inout b: T)
這個函數的泛型版本使用了佔位類型名字(通常此情況下用字母T
來表示)來代替實際類型名(如In
、String
或Doubl
)。佔位類型名沒有提示T
必須是什麼類型,但是它提示了a
和b
必須是同一類型T
,而不管T
表示什麼類型。只有swapTwoValues
函數在每次調用時所傳入的實際類型才能決定T
所代表的類型。
另外一個不同之處在於這個泛型函數名後面跟着的展位類型名字(T)是用尖括號括起來的()。這個尖括號告訴 Swift 那個T
是swapTwoValues
函數所定義的一個類型。因爲T
是一個佔位命名類型,Swift
不會去查找命名爲T的實際類型。
swapTwoValues
函數除了要求傳入的兩個任何類型值是同一類型外,也可以作爲swapTwoInts
函數被調用。每次swapTwoValues
被調用,T所代表的類型值都會傳給函數。
在下面的兩個例子中,T
分別代表Int
和String
:
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"
注意 上面定義的函數
swapTwoValues
是受swap
函數啓發而實現的。swap
函數存在於 Swift 標準庫,並可以在其它類中任意使用。如果你在自己代碼中需要類似swapTwoValues
函數的功能,你可以使用已存在的交換函數swap
函數。
類型參數
在上面的swapTwoValues
例子中,佔位類型T
是一種類型參數的示例。類型參數指定並命名爲一個佔位類型,並且緊隨在函數名後面,使用一對尖括號括起來(如)。
一旦一個類型參數被指定,那麼其可以被使用來定義一個函數的參數類型(如swapTwoValues
函數中的參數a
和b
),或作爲一個函數返回類型,或用作函數主體中的註釋類型。在這種情況下,被類型參數所代表的佔位類型不管函數任何時候被調用,都會被實際類型所替換(在上面swapTwoValues
例子中,當函數第一次被調用時,T
被Int
替換,第二次調用時,被String
替換。)。
你可支持多個類型參數,命名在尖括號中,用逗號分開。
命名類型參數
在簡單的情況下,泛型函數或泛型類型需要指定一個佔位類型(如上面的swapTwoValues
泛型函數,或一個存儲單一類型的泛型集,如數組),通常用一單個字母T
來命名類型參數。不過,你可以使用任何有效的標識符來作爲類型參數名。
如果你使用多個參數定義更復雜的泛型函數或泛型類型,那麼使用更多的描述類型參數是非常有用的。例如,Swift 字典(Dictionary)類型有兩個類型參數,一個是鍵,另外一個是值。如果你自己寫字典,你或許會定義這兩個類型參數爲KeyType
和ValueType
,用來記住它們在你的泛型代碼中的作用。
注意 請始終使用大寫字母開頭的駝峯式命名法(例如
T
和KeyType
)來給類型參數命名,以表明它們是類型的佔位符,而非類型值。
泛型類型
通常在泛型函數中,Swift
允許你定義你自己的泛型類型。這些自定義類、結構體和枚舉作用於任何類型,如同Array
和Dictionary
的用法。
這部分向你展示如何寫一個泛型集類型–Stack
(棧)。一個棧是一系列值域的集合,和Array
(數組)類似,但其是一個比
Swift 的Array
類型更多限制的集合。一個數組可以允許其裏面任何位置的插入/刪除操作,而棧,只允許在集合的末端添加新的項(如同push一個新值進棧)。同樣的一個棧也只能從末端移除項(如同pop一個值出棧)。
注意 棧的概念已被
UINavigationController
類使用來模擬試圖控制器的導航結構。你通過調用UINavigationController
的pushViewController:animated:
方法來爲導航棧添加(add)新的試圖控制器;而通過popViewControllerAnimated:
的方法來從導航棧中移除(pop)某個試圖控制器。每當你需要一個嚴格的後進先出
方式來管理集合,堆棧都是最實用的模型。
下圖展示了一個棧的壓棧(push)/出棧(pop)的行爲:
![此處輸入圖片的描述][2]
- 現在有三個值在棧中;
- 第四個值“pushed”到棧的頂部;
- 現在有四個值在棧中,最近的那個在頂部;
- 棧中最頂部的那個項被移除,或稱之爲“popped”;
- 移除掉一個值後,現在棧又重新只有三個值。
這裏展示瞭如何寫一個非泛型版本的棧,Int
值型的棧:
struct IntStack {
var items = Int[]()
mutating func push(item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
}
這個結構體在棧中使用一個Array
性質的items
存儲值。Stack
提供兩個方法:push
和pop
,從棧中壓進一個值和移除一個值。這些方法標記爲可變的,因爲他們需要修改(或轉換)結構體的items
數組。
上面所展現的IntStack
類型只能用於Int
值,不過,其對於定義一個泛型Stack
類(可以處理任何類型值的棧)是非常有用的。
這裏是一個相同代碼的泛型版本:
struct Stack<T> {
var items = T[]()
mutating func push(item: T) {
items.append(item)
}
mutating func pop() -> T {
return items.removeLast()
}
}
注意到Stack
的泛型版本基本上和非泛型版本相同,但是泛型版本的佔位類型參數爲T代替了實際Int
類型。這種類型參數包含在一對尖括號裏(<T>
),緊隨在結構體名字後面。
T
定義了一個名爲“某種類型T”的節點提供給後來用。這種將來類型可以在結構體的定義裏任何地方表示爲“T”。在這種情況下,T
在如下三個地方被用作節點:
-
創建一個名爲
items
的屬性,使用空的T類型值數組對其進行初始化; -
指定一個包含一個參數名爲
item
的push
方法,該參數必須是T類型; -
指定一個
pop
方法的返回值,該返回值將是一個T類型值。
當創建一個新單例並初始化時, 通過用一對緊隨在類型名後的尖括號裏寫出實際指定棧用到類型,創建一個Stack
實例,同創建Array
和Dictionary
一樣:
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// 現在棧已經有4個string了
下圖將展示stackOfStrings
如何push
這四個值進棧的過程:
![此處輸入圖片的描述][3]
從棧中pop
並移除值”cuatro”:
let fromTheTop = stackOfStrings.pop()
// fromTheTop is equal to "cuatro", and the stack now contains 3 strings
下圖展示瞭如何從棧中pop一個值的過程: ![此處輸入圖片的描述][4]
由於Stack
是泛型類型,所以在
Swift 中其可以用來創建任何有效類型的棧,這種方式如同Array
和Dictionary
。
類型約束
swapTwoValues
函數和Stack
類型可以作用於任何類型,不過,有的時候對使用在泛型函數和泛型類型上的類型強制約束爲某種特定類型是非常有用的。類型約束指定了一個必須繼承自指定類的類型參數,或者遵循一個特定的協議或協議構成。
例如,Swift 的Dictionary
類型對作用於其鍵的類型做了些限制。在[字典][5]的描述中,字典的鍵類型必須是可哈希,也就是說,必須有一種方法可以使其是唯一的表示。Dictionary
之所以需要其鍵是可哈希是爲了以便於其檢查其是否包含某個特定鍵的值。如無此需求,Dictionary
即不會告訴是否插入或者替換了某個特定鍵的值,也不能查找到已經存儲在字典裏面的給定鍵值。
這個需求強制加上一個類型約束作用於Dictionary
的鍵上,當然其鍵類型必須遵循Hashable
協議(Swift
標準庫中定義的一個特定協議)。所有的 Swift 基本類型(如String
,Int
, Double
和 Bool
)默認都是可哈希。
當你創建自定義泛型類型時,你可以定義你自己的類型約束,當然,這些約束要支持泛型編程的強力特徵中的多數。抽象概念如可哈希
具有的類型特徵是根據他們概念特徵來界定的,而不是他們的直接類型特徵。
類型約束語法
你可以寫一個在一個類型參數名後面的類型約束,通過冒號分割,來作爲類型參數鏈的一部分。這種作用於泛型函數的類型約束的基礎語法如下所示(和泛型類型的語法相同):
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// function body goes here
}
上面這個假定函數有兩個類型參數。第一個類型參數T
,有一個需要T
必須是SomeClass
子類的類型約束;第二個類型參數U
,有一個需要U
必須遵循SomeProtocol
協議的類型約束。
類型約束行爲
這裏有個名爲findStringIndex
的非泛型函數,該函數功能是去查找包含一給定String
值的數組。若查找到匹配的字符串,findStringIndex
函數返回該字符串在數組中的索引值(Int
),反之則返回nil
:
func findStringIndex(array: String[], valueToFind: String) -> Int? {
for (index, value) in enumerate(array) {
if value == valueToFind {
return index
}
}
return nil
}
findStringIndex
函數可以作用於查找一字符串數組中的某個字符串:
let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
if let foundIndex = findStringIndex(strings, "llama") {
println("The index of llama is \(foundIndex)")
}
// 輸出 "The index of llama is 2"
如果只是針對字符串而言查找在數組中的某個值的索引,用處不是很大,不過,你可以寫出相同功能的泛型函數findIndex
,用某個類型T
值替換掉提到的字符串。
這裏展示如何寫一個你或許期望的findStringIndex
的泛型版本findIndex
。請注意這個函數仍然返回Int
,是不是有點迷惑呢,而不是泛型類型?那是因爲函數返回的是一個可選的索引數,而不是從數組中得到的一個可選值。需要提醒的是,這個函數不會編譯,原因在例子後面會說明:
func findIndex<T>(array: T[], valueToFind: T) -> Int? {
for (index, value) in enumerate(array) {
if value == valueToFind {
return index
}
}
return nil
}
上面所寫的函數不會編譯。這個問題的位置在等式的檢查上,“if
value == valueToFind”
。不是所有的 Swift 中的類型都可以用等式符(==)進行比較。例如,如果你創建一個你自己的類或結構體來表示一個複雜的數據模型,那麼 Swift 沒法猜到對於這個類或結構體而言“等於”的意思。正因如此,這部分代碼不能可能保證工作於每個可能的類型T
,當你試圖編譯這部分代碼時估計會出現相應的錯誤。
不過,所有的這些並不會讓我們無從下手。Swift 標準庫中定義了一個Equatable
協議,該協議要求任何遵循的類型實現等式符(==)和不等符(!=)對任何兩個該類型進行比較。所有的
Swift 標準類型自動支持Equatable
協議。
任何Equatable
類型都可以安全的使用在findIndex
函數中,因爲其保證支持等式操作。爲了說明這個事實,當你定義一個函數時,你可以寫一個Equatable
類型約束作爲類型參數定義的一部分:
func findIndex<T: Equatable>(array: T[], valueToFind: T) -> Int? {
for (index, value) in enumerate(array) {
if value == valueToFind {
return index
}
}
return nil
}
findIndex
中這個單個類型參數寫做:T:
Equatable
,也就意味着“任何T類型都遵循Equatable
協議”。
findIndex
函數現在則可以成功的編譯過,並且作用於任何遵循Equatable
的類型,如Double
或String
:
let doubleIndex = findIndex([3.14159, 0.1, 0.25], 9.3)
// doubleIndex is an optional Int with no value, because 9.3 is not in the array
let stringIndex = findIndex(["Mike", "Malcolm", "Andrea"], "Andrea")
// stringIndex is an optional Int containing a value of 2
關聯類型
當定義一個協議時,有的時候聲明一個或多個關聯類型作爲協議定義的一部分是非常有用的。一個關聯類型給定作用於協議部分的類型一個節點名(或別名)。作用於關聯類型上實際類型是不需要指定的,直到該協議接受。關聯類型被指定爲typealias
關鍵字。
關聯類型行爲
這裏是一個Container
協議的例子,定義了一個ItemType關聯類型:
protocol Container {
typealias ItemType
mutating func append(item: ItemType)
var count: Int { get }
subscript(i: Int) -> ItemType { get }
}
Container
協議定義了三個任何容器必須支持的兼容要求:
-
必須可能通過
append
方法添加一個新item到容器裏; -
必須可能通過使用
count
屬性獲取容器裏items的數量,並返回一個Int
值; -
必須可能通過容器的
Int
索引值下標可以檢索到每一個item。
這個協議沒有指定容器裏item是如何存儲的或何種類型是允許的。這個協議只指定三個任何遵循Container
類型所必須支持的功能點。一個遵循的類型也可以提供其他額外的功能,只要滿足這三個條件。
任何遵循Container
協議的類型必須指定存儲在其裏面的值類型,必須保證只有正確類型的items可以加進容器裏,必須明確可以通過其下標返回item類型。
爲了定義這三個條件,Container
協議需要一個方法指定容器裏的元素將會保留,而不需要知道特定容器的類型。Container
協議需要指定任何通過append
方法添加到容器裏的值和容器裏元素是相同類型,並且通過容器下標返回的容器元素類型的值的類型是相同類型。
爲了達到此目的,Container
協議聲明了一個ItemType的關聯類型,寫作typealias
ItemType
。The protocol does not define what ItemType is an alias for—that information is left for any conforming type to provide(這個協議不會定義ItemType
是遵循類型所提供的何種信息的別名)。儘管如此,ItemType
別名支持一種方法識別在一個容器裏的items類型,以及定義一種使用在append
方法和下標中的類型,以便保證任何期望的Container
的行爲是強制性的。
這裏是一個早前IntStack類型的非泛型版本,適用於遵循Container協議:
struct IntStack: Container {
// original IntStack 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 ItemType = Int
mutating func append(item: Int) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Int {
return items[i]
}
}
IntStack
類型實現了Container
協議的所有三個要求,在IntStack
類型的每個包含部分的功能都滿足這些要求。
此外,IntStack
指定了Container
的實現,適用的ItemType被用作Int
類型。對於這個Container
協議實現而言,定義 typealias
ItemType = Int
,將抽象的ItemType
類型轉換爲具體的Int
類型。
感謝Swift類型參考,你不用在IntStack
定義部分聲明一個具體的Int
的ItemType
。由於IntStack
遵循Container
協議的所有要求,只要通過簡單的查找append
方法的item參數類型和下標返回的類型,Swift就可以推斷出合適的ItemType
來使用。確實,如果上面的代碼中你刪除了 typealias
ItemType = Int
這一行,一切仍舊可以工作,因爲它清楚的知道ItemType使用的是何種類型。
你也可以生成遵循Container
協議的泛型Stack
類型:
struct Stack<T>: Container {
// original Stack<T> implementation
var items = T[]()
mutating func push(item: T) {
items.append(item)
}
mutating func pop() -> T {
return items.removeLast()
}
// conformance to the Container protocol
mutating func append(item: T) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> T {
return items[i]
}
}
這個時候,佔位類型參數T
被用作append
方法的item參數和下標的返回類型。Swift
因此可以推斷出被用作這個特定容器的ItemType
的T
的合適類型。
擴展一個存在的類型爲一指定關聯類型
在[使用擴展來添加協議兼容性][6]中有描述擴展一個存在的類型添加遵循一個協議。這個類型包含一個關聯類型的協議。
Swift的Array
已經提供append
方法,一個count
屬性和通過下標來查找一個自己的元素。這三個功能都達到Container
協議的要求。也就意味着你可以擴展Array
去遵循Container
協議,只要通過簡單聲明Array
適用於該協議而已。如何實踐這樣一個空擴展,在[使用擴展來聲明協議的採納][7]中有描述這樣一個實現一個空擴展的行爲:
extension Array: Container {}
如同上面的泛型Stack
類型一樣,Array的append
方法和下標保證Swift
可以推斷出ItemType
所使用的適用的類型。定義了這個擴展後,你可以將任何Array
當作Container
來使用。
Where 語句
[類型約束][8]中描述的類型約束確保你定義關於類型參數的需求和一泛型函數或類型有關聯。
對於關聯類型的定義需求也是非常有用的。你可以通過這樣去定義where語句作爲一個類型參數隊列的一部分。一個where
語句使你能夠要求一個關聯類型遵循一個特定的協議,以及(或)那個特定的類型參數和關聯類型可以是相同的。你可寫一個where
語句,通過緊隨放置where
關鍵字在類型參數隊列後面,其後跟着一個或者多個針對關聯類型的約束,以及(或)一個或多個類型和關聯類型的等於關係。
下面的列子定義了一個名爲allItemsMatch
的泛型函數,用來檢查是否兩個Container
單例包含具有相同順序的相同元素。如果匹配到所有的元素,那麼返回一個爲true
的Boolean
值,反之,則相反。
這兩個容器可以被檢查出是否是相同類型的容器(雖然它們可以是),但他們確實擁有相同類型的元素。這個需求通過一個類型約束和where
語句結合來表示:
func allItemsMatch<
C1: Container, C2: Container
where C1.ItemType == C2.ItemType, C1.ItemType: Equatable>
(someContainer: C1, anotherContainer: C2) -> Bool {
// 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
}
這個函數用了兩個參數:someContainer
和anotherContainer
。someContainer
參數是類型C1
,anotherContainer
參數是類型C2
。C1
和C2
是容器的兩個佔位類型參數,決定了這個函數何時被調用。
這個函數的類型參數列緊隨在兩個類型參數需求的後面:
-
C1
必須遵循Container
協議 (寫作C1: Container
)。 -
C2
必須遵循Container
協議 (寫作C2: Container
)。 -
C1
的ItemType
同樣是C2的ItemType
(寫作C1.ItemType == C2.ItemType
)。 -
C1
的ItemType
必須遵循Equatable
協議 (寫作C1.ItemType: Equatable
)。
第三個和第四個要求被定義爲一個where
語句的一部分,寫在關鍵字where
後面,作爲函數類型參數鏈的一部分。
這些要求意思是:
someContainer
是一個C1
類型的容器。 anotherContainer
是一個C2
類型的容器。 someContainer
和anotherContainer
包含相同的元素類型。 someContainer
中的元素可以通過不等於操作(!=
)來檢查它們是否彼此不同。
第三個和第四個要求結合起來的意思是anotherContainer
中的元素也可以通過 !=
操作來檢查,因爲他們在someContainer
中元素確實是相同的類型。
這些要求能夠使allItemsMatch
函數比較兩個容器,即便他們是不同的容器類型。
allItemsMatch
首先檢查兩個容器是否擁有同樣數目的items,如果他們的元素數目不同,沒有辦法進行匹配,函數就會false
。
檢查完之後,函數通過for-in
循環和半閉區間操作(..)來迭代someContainer
中的所有元素。對於每個元素,函數檢查是否someContainer
中的元素不等於對應的anotherContainer
中的元素,如果這兩個元素不等,則這兩個容器不匹配,返回false
。
如果循環體結束後未發現沒有任何的不匹配,那表明兩個容器匹配,函數返回true
。
這裏演示了allItemsMatch函數運算的過程:
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
var arrayOfStrings = ["uno", "dos", "tres"]
if allItemsMatch(stackOfStrings, arrayOfStrings) {
println("All items match.")
} else {
println("Not all items match.")
}
// 輸出 "All items match."
上面的例子創建一個Stack
單例來存儲String
,然後壓了三個字符串進棧。這個例子也創建了一個Array
單例,並初始化包含三個同棧裏一樣的原始字符串。即便棧和數組否是不同的類型,但他們都遵循Container
協議,而且他們都包含同樣的類型值。你因此可以調用allItemsMatch
函數,用這兩個容器作爲它的參數。在上面的例子中,allItemsMatch
函數正確的顯示了所有的這兩個容器的items
匹配。
泛型函數
泛型函數
可以工作於任何類型,這裏是一個上面swapTwoInts
函數的泛型版本,用於交換兩個值:
func swapTwoValues<T>(inout a: T, inout b: T) {
let temporaryA = a
a = b
b = temporaryA
}
swapTwoValues
函數主體和swapTwoInts
函數是一樣的,它只在第一行稍微有那麼一點點不同於swapTwoInts
,如下所示:
func swapTwoInts(inout a: Int, inout b: Int)
func swapTwoValues<T>(inout a: T, inout b: T)
這個函數的泛型版本使用了佔位類型名字(通常此情況下用字母T
來表示)來代替實際類型名(如In
、String
或Doubl
)。佔位類型名沒有提示T
必須是什麼類型,但是它提示了a
和b
必須是同一類型T
,而不管T
表示什麼類型。只有swapTwoValues
函數在每次調用時所傳入的實際類型才能決定T
所代表的類型。
另外一個不同之處在於這個泛型函數名後面跟着的展位類型名字(T)是用尖括號括起來的()。這個尖括號告訴 Swift 那個T
是swapTwoValues
函數所定義的一個類型。因爲T
是一個佔位命名類型,Swift
不會去查找命名爲T的實際類型。
swapTwoValues
函數除了要求傳入的兩個任何類型值是同一類型外,也可以作爲swapTwoInts
函數被調用。每次swapTwoValues
被調用,T所代表的類型值都會傳給函數。
在下面的兩個例子中,T
分別代表Int
和String
:
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"
注意 上面定義的函數
swapTwoValues
是受swap
函數啓發而實現的。swap
函數存在於 Swift 標準庫,並可以在其它類中任意使用。如果你在自己代碼中需要類似swapTwoValues
函數的功能,你可以使用已存在的交換函數swap
函數。
類型參數
在上面的swapTwoValues
例子中,佔位類型T
是一種類型參數的示例。類型參數指定並命名爲一個佔位類型,並且緊隨在函數名後面,使用一對尖括號括起來(如)。
一旦一個類型參數被指定,那麼其可以被使用來定義一個函數的參數類型(如swapTwoValues
函數中的參數a
和b
),或作爲一個函數返回類型,或用作函數主體中的註釋類型。在這種情況下,被類型參數所代表的佔位類型不管函數任何時候被調用,都會被實際類型所替換(在上面swapTwoValues
例子中,當函數第一次被調用時,T
被Int
替換,第二次調用時,被String
替換。)。
你可支持多個類型參數,命名在尖括號中,用逗號分開。
命名類型參數
在簡單的情況下,泛型函數或泛型類型需要指定一個佔位類型(如上面的swapTwoValues
泛型函數,或一個存儲單一類型的泛型集,如數組),通常用一單個字母T
來命名類型參數。不過,你可以使用任何有效的標識符來作爲類型參數名。
如果你使用多個參數定義更復雜的泛型函數或泛型類型,那麼使用更多的描述類型參數是非常有用的。例如,Swift 字典(Dictionary)類型有兩個類型參數,一個是鍵,另外一個是值。如果你自己寫字典,你或許會定義這兩個類型參數爲KeyType
和ValueType
,用來記住它們在你的泛型代碼中的作用。
注意 請始終使用大寫字母開頭的駝峯式命名法(例如
T
和KeyType
)來給類型參數命名,以表明它們是類型的佔位符,而非類型值。
泛型類型
通常在泛型函數中,Swift
允許你定義你自己的泛型類型。這些自定義類、結構體和枚舉作用於任何類型,如同Array
和Dictionary
的用法。
這部分向你展示如何寫一個泛型集類型–Stack
(棧)。一個棧是一系列值域的集合,和Array
(數組)類似,但其是一個比
Swift 的Array
類型更多限制的集合。一個數組可以允許其裏面任何位置的插入/刪除操作,而棧,只允許在集合的末端添加新的項(如同push一個新值進棧)。同樣的一個棧也只能從末端移除項(如同pop一個值出棧)。
注意 棧的概念已被
UINavigationController
類使用來模擬試圖控制器的導航結構。你通過調用UINavigationController
的pushViewController:animated:
方法來爲導航棧添加(add)新的試圖控制器;而通過popViewControllerAnimated:
的方法來從導航棧中移除(pop)某個試圖控制器。每當你需要一個嚴格的後進先出
方式來管理集合,堆棧都是最實用的模型。
下圖展示了一個棧的壓棧(push)/出棧(pop)的行爲:
![此處輸入圖片的描述][2]
- 現在有三個值在棧中;
- 第四個值“pushed”到棧的頂部;
- 現在有四個值在棧中,最近的那個在頂部;
- 棧中最頂部的那個項被移除,或稱之爲“popped”;
- 移除掉一個值後,現在棧又重新只有三個值。
這裏展示瞭如何寫一個非泛型版本的棧,Int
值型的棧:
struct IntStack {
var items = Int[]()
mutating func push(item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
}
這個結構體在棧中使用一個Array
性質的items
存儲值。Stack
提供兩個方法:push
和pop
,從棧中壓進一個值和移除一個值。這些方法標記爲可變的,因爲他們需要修改(或轉換)結構體的items
數組。
上面所展現的IntStack
類型只能用於Int
值,不過,其對於定義一個泛型Stack
類(可以處理任何類型值的棧)是非常有用的。
這裏是一個相同代碼的泛型版本:
struct Stack<T> {
var items = T[]()
mutating func push(item: T) {
items.append(item)
}
mutating func pop() -> T {
return items.removeLast()
}
}
注意到Stack
的泛型版本基本上和非泛型版本相同,但是泛型版本的佔位類型參數爲T代替了實際Int
類型。這種類型參數包含在一對尖括號裏(<T>
),緊隨在結構體名字後面。
T
定義了一個名爲“某種類型T”的節點提供給後來用。這種將來類型可以在結構體的定義裏任何地方表示爲“T”。在這種情況下,T
在如下三個地方被用作節點:
-
創建一個名爲
items
的屬性,使用空的T類型值數組對其進行初始化; -
指定一個包含一個參數名爲
item
的push
方法,該參數必須是T類型; -
指定一個
pop
方法的返回值,該返回值將是一個T類型值。
當創建一個新單例並初始化時, 通過用一對緊隨在類型名後的尖括號裏寫出實際指定棧用到類型,創建一個Stack
實例,同創建Array
和Dictionary
一樣:
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// 現在棧已經有4個string了
下圖將展示stackOfStrings
如何push
這四個值進棧的過程:
![此處輸入圖片的描述][3]
從棧中pop
並移除值”cuatro”:
let fromTheTop = stackOfStrings.pop()
// fromTheTop is equal to "cuatro", and the stack now contains 3 strings
下圖展示瞭如何從棧中pop一個值的過程: ![此處輸入圖片的描述][4]
由於Stack
是泛型類型,所以在
Swift 中其可以用來創建任何有效類型的棧,這種方式如同Array
和Dictionary
。
類型約束
swapTwoValues
函數和Stack
類型可以作用於任何類型,不過,有的時候對使用在泛型函數和泛型類型上的類型強制約束爲某種特定類型是非常有用的。類型約束指定了一個必須繼承自指定類的類型參數,或者遵循一個特定的協議或協議構成。
例如,Swift 的Dictionary
類型對作用於其鍵的類型做了些限制。在[字典][5]的描述中,字典的鍵類型必須是可哈希,也就是說,必須有一種方法可以使其是唯一的表示。Dictionary
之所以需要其鍵是可哈希是爲了以便於其檢查其是否包含某個特定鍵的值。如無此需求,Dictionary
即不會告訴是否插入或者替換了某個特定鍵的值,也不能查找到已經存儲在字典裏面的給定鍵值。
這個需求強制加上一個類型約束作用於Dictionary
的鍵上,當然其鍵類型必須遵循Hashable
協議(Swift
標準庫中定義的一個特定協議)。所有的 Swift 基本類型(如String
,Int
, Double
和 Bool
)默認都是可哈希。
當你創建自定義泛型類型時,你可以定義你自己的類型約束,當然,這些約束要支持泛型編程的強力特徵中的多數。抽象概念如可哈希
具有的類型特徵是根據他們概念特徵來界定的,而不是他們的直接類型特徵。
類型約束語法
你可以寫一個在一個類型參數名後面的類型約束,通過冒號分割,來作爲類型參數鏈的一部分。這種作用於泛型函數的類型約束的基礎語法如下所示(和泛型類型的語法相同):
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// function body goes here
}
上面這個假定函數有兩個類型參數。第一個類型參數T
,有一個需要T
必須是SomeClass
子類的類型約束;第二個類型參數U
,有一個需要U
必須遵循SomeProtocol
協議的類型約束。
類型約束行爲
這裏有個名爲findStringIndex
的非泛型函數,該函數功能是去查找包含一給定String
值的數組。若查找到匹配的字符串,findStringIndex
函數返回該字符串在數組中的索引值(Int
),反之則返回nil
:
func findStringIndex(array: String[], valueToFind: String) -> Int? {
for (index, value) in enumerate(array) {
if value == valueToFind {
return index
}
}
return nil
}
findStringIndex
函數可以作用於查找一字符串數組中的某個字符串:
let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
if let foundIndex = findStringIndex(strings, "llama") {
println("The index of llama is \(foundIndex)")
}
// 輸出 "The index of llama is 2"
如果只是針對字符串而言查找在數組中的某個值的索引,用處不是很大,不過,你可以寫出相同功能的泛型函數findIndex
,用某個類型T
值替換掉提到的字符串。
這裏展示如何寫一個你或許期望的findStringIndex
的泛型版本findIndex
。請注意這個函數仍然返回Int
,是不是有點迷惑呢,而不是泛型類型?那是因爲函數返回的是一個可選的索引數,而不是從數組中得到的一個可選值。需要提醒的是,這個函數不會編譯,原因在例子後面會說明:
func findIndex<T>(array: T[], valueToFind: T) -> Int? {
for (index, value) in enumerate(array) {
if value == valueToFind {
return index
}
}
return nil
}
上面所寫的函數不會編譯。這個問題的位置在等式的檢查上,“if
value == valueToFind”
。不是所有的 Swift 中的類型都可以用等式符(==)進行比較。例如,如果你創建一個你自己的類或結構體來表示一個複雜的數據模型,那麼 Swift 沒法猜到對於這個類或結構體而言“等於”的意思。正因如此,這部分代碼不能可能保證工作於每個可能的類型T
,當你試圖編譯這部分代碼時估計會出現相應的錯誤。
不過,所有的這些並不會讓我們無從下手。Swift 標準庫中定義了一個Equatable
協議,該協議要求任何遵循的類型實現等式符(==)和不等符(!=)對任何兩個該類型進行比較。所有的
Swift 標準類型自動支持Equatable
協議。
任何Equatable
類型都可以安全的使用在findIndex
函數中,因爲其保證支持等式操作。爲了說明這個事實,當你定義一個函數時,你可以寫一個Equatable
類型約束作爲類型參數定義的一部分:
func findIndex<T: Equatable>(array: T[], valueToFind: T) -> Int? {
for (index, value) in enumerate(array) {
if value == valueToFind {
return index
}
}
return nil
}
findIndex
中這個單個類型參數寫做:T:
Equatable
,也就意味着“任何T類型都遵循Equatable
協議”。
findIndex
函數現在則可以成功的編譯過,並且作用於任何遵循Equatable
的類型,如Double
或String
:
let doubleIndex = findIndex([3.14159, 0.1, 0.25], 9.3)
// doubleIndex is an optional Int with no value, because 9.3 is not in the array
let stringIndex = findIndex(["Mike", "Malcolm", "Andrea"], "Andrea")
// stringIndex is an optional Int containing a value of 2
關聯類型
當定義一個協議時,有的時候聲明一個或多個關聯類型作爲協議定義的一部分是非常有用的。一個關聯類型給定作用於協議部分的類型一個節點名(或別名)。作用於關聯類型上實際類型是不需要指定的,直到該協議接受。關聯類型被指定爲typealias
關鍵字。
關聯類型行爲
這裏是一個Container
協議的例子,定義了一個ItemType關聯類型:
protocol Container {
typealias ItemType
mutating func append(item: ItemType)
var count: Int { get }
subscript(i: Int) -> ItemType { get }
}
Container
協議定義了三個任何容器必須支持的兼容要求:
-
必須可能通過
append
方法添加一個新item到容器裏; -
必須可能通過使用
count
屬性獲取容器裏items的數量,並返回一個Int
值; -
必須可能通過容器的
Int
索引值下標可以檢索到每一個item。
這個協議沒有指定容器裏item是如何存儲的或何種類型是允許的。這個協議只指定三個任何遵循Container
類型所必須支持的功能點。一個遵循的類型也可以提供其他額外的功能,只要滿足這三個條件。
任何遵循Container
協議的類型必須指定存儲在其裏面的值類型,必須保證只有正確類型的items可以加進容器裏,必須明確可以通過其下標返回item類型。
爲了定義這三個條件,Container
協議需要一個方法指定容器裏的元素將會保留,而不需要知道特定容器的類型。Container
協議需要指定任何通過append
方法添加到容器裏的值和容器裏元素是相同類型,並且通過容器下標返回的容器元素類型的值的類型是相同類型。
爲了達到此目的,Container
協議聲明了一個ItemType的關聯類型,寫作typealias
ItemType
。The protocol does not define what ItemType is an alias for—that information is left for any conforming type to provide(這個協議不會定義ItemType
是遵循類型所提供的何種信息的別名)。儘管如此,ItemType
別名支持一種方法識別在一個容器裏的items類型,以及定義一種使用在append
方法和下標中的類型,以便保證任何期望的Container
的行爲是強制性的。
這裏是一個早前IntStack類型的非泛型版本,適用於遵循Container協議:
struct IntStack: Container {
// original IntStack 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 ItemType = Int
mutating func append(item: Int) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Int {
return items[i]
}
}
IntStack
類型實現了Container
協議的所有三個要求,在IntStack
類型的每個包含部分的功能都滿足這些要求。
此外,IntStack
指定了Container
的實現,適用的ItemType被用作Int
類型。對於這個Container
協議實現而言,定義 typealias
ItemType = Int
,將抽象的ItemType
類型轉換爲具體的Int
類型。
感謝Swift類型參考,你不用在IntStack
定義部分聲明一個具體的Int
的ItemType
。由於IntStack
遵循Container
協議的所有要求,只要通過簡單的查找append
方法的item參數類型和下標返回的類型,Swift就可以推斷出合適的ItemType
來使用。確實,如果上面的代碼中你刪除了 typealias
ItemType = Int
這一行,一切仍舊可以工作,因爲它清楚的知道ItemType使用的是何種類型。
你也可以生成遵循Container
協議的泛型Stack
類型:
struct Stack<T>: Container {
// original Stack<T> implementation
var items = T[]()
mutating func push(item: T) {
items.append(item)
}
mutating func pop() -> T {
return items.removeLast()
}
// conformance to the Container protocol
mutating func append(item: T) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> T {
return items[i]
}
}
這個時候,佔位類型參數T
被用作append
方法的item參數和下標的返回類型。Swift
因此可以推斷出被用作這個特定容器的ItemType
的T
的合適類型。
擴展一個存在的類型爲一指定關聯類型
在[使用擴展來添加協議兼容性][6]中有描述擴展一個存在的類型添加遵循一個協議。這個類型包含一個關聯類型的協議。
Swift的Array
已經提供append
方法,一個count
屬性和通過下標來查找一個自己的元素。這三個功能都達到Container
協議的要求。也就意味着你可以擴展Array
去遵循Container
協議,只要通過簡單聲明Array
適用於該協議而已。如何實踐這樣一個空擴展,在[使用擴展來聲明協議的採納][7]中有描述這樣一個實現一個空擴展的行爲:
extension Array: Container {}
如同上面的泛型Stack
類型一樣,Array的append
方法和下標保證Swift
可以推斷出ItemType
所使用的適用的類型。定義了這個擴展後,你可以將任何Array
當作Container
來使用。
Where 語句
[類型約束][8]中描述的類型約束確保你定義關於類型參數的需求和一泛型函數或類型有關聯。
對於關聯類型的定義需求也是非常有用的。你可以通過這樣去定義where語句作爲一個類型參數隊列的一部分。一個where
語句使你能夠要求一個關聯類型遵循一個特定的協議,以及(或)那個特定的類型參數和關聯類型可以是相同的。你可寫一個where
語句,通過緊隨放置where
關鍵字在類型參數隊列後面,其後跟着一個或者多個針對關聯類型的約束,以及(或)一個或多個類型和關聯類型的等於關係。
下面的列子定義了一個名爲allItemsMatch
的泛型函數,用來檢查是否兩個Container
單例包含具有相同順序的相同元素。如果匹配到所有的元素,那麼返回一個爲true
的Boolean
值,反之,則相反。
這兩個容器可以被檢查出是否是相同類型的容器(雖然它們可以是),但他們確實擁有相同類型的元素。這個需求通過一個類型約束和where
語句結合來表示:
func allItemsMatch<
C1: Container, C2: Container
where C1.ItemType == C2.ItemType, C1.ItemType: Equatable>
(someContainer: C1, anotherContainer: C2) -> Bool {
// 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
}
這個函數用了兩個參數:someContainer
和anotherContainer
。someContainer
參數是類型C1
,anotherContainer
參數是類型C2
。C1
和C2
是容器的兩個佔位類型參數,決定了這個函數何時被調用。
這個函數的類型參數列緊隨在兩個類型參數需求的後面:
-
C1
必須遵循Container
協議 (寫作C1: Container
)。 -
C2
必須遵循Container
協議 (寫作C2: Container
)。 -
C1
的ItemType
同樣是C2的ItemType
(寫作C1.ItemType == C2.ItemType
)。 -
C1
的ItemType
必須遵循Equatable
協議 (寫作C1.ItemType: Equatable
)。
第三個和第四個要求被定義爲一個where
語句的一部分,寫在關鍵字where
後面,作爲函數類型參數鏈的一部分。
這些要求意思是:
someContainer
是一個C1
類型的容器。 anotherContainer
是一個C2
類型的容器。 someContainer
和anotherContainer
包含相同的元素類型。 someContainer
中的元素可以通過不等於操作(!=
)來檢查它們是否彼此不同。
第三個和第四個要求結合起來的意思是anotherContainer
中的元素也可以通過 !=
操作來檢查,因爲他們在someContainer
中元素確實是相同的類型。
這些要求能夠使allItemsMatch
函數比較兩個容器,即便他們是不同的容器類型。
allItemsMatch
首先檢查兩個容器是否擁有同樣數目的items,如果他們的元素數目不同,沒有辦法進行匹配,函數就會false
。
檢查完之後,函數通過for-in
循環和半閉區間操作(..)來迭代someContainer
中的所有元素。對於每個元素,函數檢查是否someContainer
中的元素不等於對應的anotherContainer
中的元素,如果這兩個元素不等,則這兩個容器不匹配,返回false
。
如果循環體結束後未發現沒有任何的不匹配,那表明兩個容器匹配,函數返回true
。
這裏演示了allItemsMatch函數運算的過程:
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
var arrayOfStrings = ["uno", "dos", "tres"]
if allItemsMatch(stackOfStrings, arrayOfStrings) {
println("All items match.")
} else {
println("Not all items match.")
}
// 輸出 "All items match."
上面的例子創建一個Stack
單例來存儲String
,然後壓了三個字符串進棧。這個例子也創建了一個Array
單例,並初始化包含三個同棧裏一樣的原始字符串。即便棧和數組否是不同的類型,但他們都遵循Container
協議,而且他們都包含同樣的類型值。你因此可以調用allItemsMatch
函數,用這兩個容器作爲它的參數。在上面的例子中,allItemsMatch
函數正確的顯示了所有的這兩個容器的items
匹配。