【Swift 3.1】23 - 泛型 (Generics)
自從蘋果2014年發(fā)布Swift眯亦,到現(xiàn)在已經(jīng)兩年多了侮叮,而Swift也來到了3.1版本姿搜。去年利用工作之余笤昨,共花了兩個多月的時間把官方的Swift編程指南看完×媒溃現(xiàn)在整理一下筆記停士,回顧一下以前的知識。有需要的同學可以去看官方文檔>>完丽。
泛型代碼能讓我們編寫靈活恋技、可重復使用的方法和類型÷咦澹可以避免編寫重復代碼蜻底,并以清晰和抽象的方法表達其目的。
泛型是Swift非常強大的一個特性聘鳞,并且很多Swift標準庫都是用泛型編寫的薄辅。實際上我們已經(jīng)使用過泛型,例如Swift的Array
和Dictionary
都是泛型集合搁痛。
泛型能解決的問題 (The Problem That Generics Solve)
下面這個方法只能交換兩個Int
值长搀,使用in-out參數(shù)來交換a
和b
:
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
上面這個方法只能交換Int
,如果我想交換兩個String
類型的值或者兩個Double
類型的值鸡典,我們又得寫兩個方法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
}
使用泛型代碼讓我們用一個方法就能解決上面的問題源请。
泛型方法 (Generic Functions)
上面的三個方法可以用下面的額泛型方法代替:
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}
這個通用的方法使用T
作為類型名稱,而不是Int
彻况、String
或者Double
谁尸。T
沒有說明必須是什么類型,但是a
和b
的類型必須是一樣的纽甘。
方法名后面還有一個<T>
良蛮,告訴Swift只是一個占位類型,不是實際類型悍赢。
swapTwoValues(_:_:)
方法可以像之前的swapTwoInts
一樣調用决瞳,只要兩個用于交換的值類型是一樣的货徙。
用通用的方法交換兩個String
類型的值或者兩個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"
注意:其實Swift的標準庫中已經(jīng)有一個功能與swapTwoValues(_:_:)
一樣的方法swap
。如果我們需要交換兩個值皮胡,直接使用swap
即可痴颊,無需自己另外實現(xiàn)這個功能。
類型參數(shù) (Type Parameters)
上面的泛型方法swapTwoValues(_:_:)
屡贺,占位類型T
其實是一個類型參數(shù)蠢棱。類型參數(shù)指定并命名占位類型,緊跟在方法名后面甩栈,用<T>
表示泻仙。
只要定義好了類型參數(shù),我們就可以用來定義方法的參數(shù)類型(例如swapTwoValues(_:_:)
的參數(shù)a
和b
的類型)量没,或者作為方法的返回類型玉转。
我們可以定義多個類型參數(shù),寫法是:<T1, T2, ...>
允蜈。
類型參數(shù)命名 (Type Parameters)
在很多情況下冤吨,類型參數(shù)有描述性的名稱,例如Dictionary<Key, Value>
中的Key
和Value
饶套,Array<Element>
中的Element
漩蟆,這些名字都能告訴讀者類型參數(shù)和泛型的關系。但是妓蛮,在沒有任何意義的情況下怠李,我們一般把類型參數(shù)名命名為T
、U
和V
蛤克。
注意:需要使用駱駝命名法給類型參數(shù)命名捺癞,例如T
和MyTypeParameter
,以表示他們是一個類型的占位构挤。
泛型類型 (Generic Types)
除了泛型方法髓介,Swift還可以定義泛型類型,例如Array
和Dictionary
筋现。
這一部分將演示如何寫一個泛型集合Stack
唐础。一個棧是一個有序的值的集合,類似一個數(shù)組矾飞,但是比數(shù)組有更嚴格的運算一膨。 數(shù)組可以在特定的位置移除或插入元素。而棧只允許在集合的最后添加元素洒沦,也只允許從最后面移除元素豹绪。
注意:棧的概念就被用于UINavigationController
管理控制器。使用pushViewController(_:animated:)
來添加新的控制器到棧中申眼,用popViewControllerAnimated(_:)
移除控制器瞒津。棧非常適合用來管理“后進先去”的集合蝉衣。
下圖演示了棧的push/pop:
- 目前棧中有三個值
- 第四個值被push到棧頂
- 棧中有四個值
- 第四個值被移除(popped)
- 移除第四個值后,棧中剩下三個值
下面是一個不通用的IntStack
:
struct IntStack {
var items = [Int]()
mutating fuc push(_ item: Int) {
items.append(item)
}
mutating fuc pop() -> Int{
return items.removeLast()
}
}
下面是一個通用版本的Stack:
struct Stack<Element> {
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
Element
定義了一個占位名字巷蚪,并且在三個地方被用到:
- 創(chuàng)建一個空數(shù)組
items
买乃,其元素類型為Element
- 作為
push(_:)
方法的參數(shù)類型 - 作為
pop()
方法的返回值
因為它是一個通用類型,所以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
移除過程如下:
擴展泛型類型 (Extending a Generic Type)
在擴展泛型類型是钓辆,我們不必提供類型參數(shù),可以直接使用已有泛型類型的類型參數(shù)肴焊。
下面是對Stack
的擴展:
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."
泛型類型的擴展也可以指定一些限定條件前联,滿足這些條件才可以訪問擴展里面的成員,下面會講到娶眷。
類型約束 (Type Constraints)
swapTwoValues(_:_:)
和Stack
可以用于任何類型似嗤。然而,在某些類型上添加一些類型約束是非常有用的届宠。類型約束指定一個類型參數(shù)必須繼承于特定的類或者遵循特定的協(xié)議烁落。
例如,Swift的Dictionary
限定了鍵的類型豌注。字典的鍵必須是hashable的伤塌,也就是說,必須提供一個方法保證它自己是唯一的轧铁,然后字典才能根據(jù)這個鍵去查找對應的值每聪。Swift的基本類型默認都是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é)議童本。
類型約束實踐 (Type Constraint in Action)
下面是一個不通用的findIndex(ofString:in:)
,用于查找指定字符串在字符串數(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
}
其實這個方法不能編譯通過脸候,因為value == valueToFind
穷娱。不是所有Swift類型的都可使用==
進行比較。
Swift定義了Equatable
協(xié)議纪他,要求所有遵循這個協(xié)議的類型必須實現(xiàn)==
和!=
運算符鄙煤。Swift的標準類型都遵循了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
關聯(lián)類型 (Associated Types)
當定義協(xié)議時梯刚,定義一個或多個關聯(lián)類型有時候是非常有用的。關聯(lián)類型提供了一個類型名字薪寓,然后這個類型可以在協(xié)議中使用亡资。使用associatedtype
關鍵字來定義關聯(lián)類型澜共。
關聯(lián)類型實踐 (Associated Types in Action)
下面是一個例子:
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
這個容器協(xié)議中沒有具體說明能存儲那個類型的值。遵循這個協(xié)議的類型必須寫清楚能存儲的值類型锥腻。
之前那個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
是一個Int
類型嗦董。typealias Item = Int
把抽象的Item
轉變?yōu)榫唧w的Int
。
因為Swift有類型推斷功能瘦黑,所以我們可以不用寫typealias Item = Int
京革,因為IntStack
實現(xiàn)了Container
協(xié)議的所有要求,Swift能推斷出需要用的Item
的具體類型幸斥。
之前那個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]
}
}
通過擴展類型來指定關聯(lián)類型 (Extending an Existing Type to Specifying an Associated Type)
我們可以使用擴展來遵循某一個協(xié)議匹摇,當然這個協(xié)議也可以是由關聯(lián)類型的協(xié)議。
Swift的Array
已經(jīng)提供了append(_:)
方法甲葬、count
屬性和下標廊勃,那么就滿足了Container
協(xié)議的要求。所以我們可以是下面這種形式來聲明Array
遵循Container
協(xié)議:
extension Array: Container {}
我們就可以把Array
當做是一個Container
经窖。
泛型的Where語句 (Generic Where Clauses)
泛型的where
語句可以讓我們要求關聯(lián)類型遵循一個特定的協(xié)議坡垫,或者關聯(lián)類型和類型參數(shù)必須相同。
例如下面這個例子:
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(_:_:)
方法的實際使用:
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語句的擴展 (Extensions with a Generic Where Clause)
例如下面這個例子:
extension Stack where Element: Equatable {
func isTop(_ item: Element) -> Bool {
guard let topItem = items.last else {
return false
}
return topItem == item
}
}
如果沒有泛型where
語句配乱,將會出現(xiàn)一個問題:isTop(_:)
方法使用了==
運算符屿脐,但是Stack
沒有要求它的元素是可以比較的,所以使用==
將會編譯錯誤宪卿。
下面是isTop(_:)
方法的使用:
if stackOfStrings.isTop("tres") {
print("Top element is tres.")
} else {
print("Top element is something else.")
}
// Prints "Top element is tres."
下面是使用where
擴展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."
上面對Container
的擴展要求Item
遵循Equatable
協(xié)議的诵,我們還可以要求Item
是一個具體的類型:
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
語句中可以添加多個約束條件,條件之間用逗號隔開即可佑钾。
第二十三部分完西疤。下個部分:【Swift 3.1】24 -訪問權限
如果有錯誤的地方,歡迎指正休溶!謝謝代赁!