二十一、泛型 Generics
1、概述
泛型是Swift中最强大的特性之一,使用泛型可以写出灵活、可重用、干净、抽象的代码,并且避免代码重复。实际上在第一章中我们就接触到了泛型,Array 和 Dictionary 是泛型容器,可以存入任何类型。
2. 泛型所要解决的问题 The Problem That Generics Solve
下面定义了一个交换两个值的函数,它没有使用泛型特性:
func swapTwoInts(inout a: Int, inout b: Int) {
let temporaryA = a
a = b
b = temporaryA
}
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
println("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// prints "someInt is now 107, and anotherInt is now 3"
函数swapTwoInts只能用于整形,再定义两个函数:
func swapTwoStrings(inout a: String, inout b: String) {
let temporaryA = a
a = b
b = temporaryA
}
func swapTwoDoubles(inout a: Double, inout b: Double) {
let temporaryA = a
a = b
b = temporaryA
}
3. 泛型方法 Generic Functions
泛型方法可以用于任何类型,将上面的方法使用泛型方法进行定义:
func swapTwoValues<T>(inout a: T, inout b: T) {
let temporaryA = a
a = b
b = temporaryA
}
比较他们的不同点:
泛 型的版本使用了一个占位符(上面的函数是 T),表示实际的类型(Int, String, Double)。占位符 T 没有表明 T 的具体类型,仅仅表明了 a 和 b 都必须是相同的类型 T。T 的真正类型只有在 swapTwoValues 被调用时才确定。
尖括号 <> 用于表明 T 是一个占位符,代表某种类型。
调用上面定义的泛型函数:
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标准库中定义了泛型函数swap,与上面的函数功能相同。
4. 泛型的参数 Type Parameters
在上面swapTwoValues
的例子中,T 就是 Type Parameter。
一旦你指定了一个 type parameter,你可以使用它来作为函数的参数类型,比如swapTwoValues
中的 a 和 b ,或者作为函数的返回值的类型。无论哪种情况,当函数实际被调用时,占位符所代表的 Type Parameter 被实际类型说替换(replace)。在上面的例子中,T先被Int替换,然后被String替换。
你可以定义多个 type paramete,写法是在尖括号中写上多个type paramete的名字,名字之间用逗号隔开。
5. 命名的泛型参数(Naming Type Parameters)
有些时候泛型函数或泛型类型只有一个占位符(泛型参数),比如上面的swapTwoValues函数,比如泛型集合Array。
但 是有时如果定义一个复杂的泛,这个泛型有多个占位符,给这type parameter names 提供更详细的描述是很有用的。比如字典类型Dictionary,它有Type Parameter——一个用于key,一个用于value,那么如果是你自己来实现字典,你会给这两个Type Parameter命名为Key何Value,以提醒我们参数的功能。
注意:泛型的类型占位符Type Parameter永远要以大写字母开头,比如 T、 Key,表明它是一个类型占位符(本质是一个类型),而不是一个值。
6. 泛型类型 Generic Types
除了泛型函数,Swift允许你定义自己的泛型类型。
本章将介绍如何定义一个泛型集合类型Stack。Stack与Array类似,但是它只允许你在集合的尾部添加数据(入栈push)和删除数据(出栈pop)。如下图所示:
定义一个不使用泛型的版本:
struct IntStack {
var items = [Int]()
mutating func push(item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
}
push和pop方法需要定义为mutating类型,因为他们要修改数组items。
上面的IntStack 只能用于整形,下面定义可以适用于任何类型的泛型版本:
struct Stack<T> {
var items = [T]()
mutating func push(item: T) {
items.append(item)
}
mutating func pop() -> T {
return items.removeLast()
}
}
type parameter T 写在<>中,紧跟结构体名之后。T表示了类型占位符名,它表示了某种将来会被使用的某种类型。
你可以通过把实际类型写到尖括号中替换T,比如,Stack<String>()
,来定义一个Stack的实例:
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// the stack now contains 4 strings
出栈:
let fromTheTop = stackOfStrings.pop()
// fromTheTop is equal to "cuatro", and the stack now contains 3 string
7. 扩展泛型类型 Extending a Generic Type
当你扩展泛型类型的时候,你不需要提供泛型的参数表parameter list,原泛型的参数可以在扩展的函数体中使用。
下面的例子扩展了上面的泛型Stack<T>,给它增加了一个computed属性 topItem,用来返回栈顶值(不出栈):
extension Stack {
var topItem: T? {
return items.isEmpty ? nil : items[items.count - 1]
}
}
访问topItem:
if let topItem = stackOfStrings.topItem {
println("The top item on the stack is \(topItem).")
}
// prints "The top item on the stack is tres."
8. 类型限制 Type Constraints
上面定义的 swapTwoValues 和
Stack 可以用于任何类型。但是有时我们需要将泛型类型或泛型函数的适用范围限制在某些特定的类型。类型限制制定了泛型参数type parameter 只能适用于某种类、某种协议(particular protocol)和实现了某种协议的类型(protocol composition)。
例如Swift中Dictionary限制了能用于Key的类型,Key必须是 hashable 的, 即Key必须能够提供一种方式让它只有唯一的表示。字典Key的类型限制通过指定Key必须实现Hashable协议来实现。Hashable协议是 Swift标准库中定义的协议,所有的Swift基本数据类型(比如String,Int,Double和Bool)默认都是hashable的。
8.1 类型限制语法 Type Constraint Syntax
在泛型参数名后加上一些类或协议来增加限制,泛型参数名与限制之间用冒号分开:
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// function body goes here
}
上面的例子给泛型参数T和U增加限制,T必须是SomeClass
的子类,U必须是实现了SomeProtocol协议(原话:requires U
to conform to the protocol )。
8.2 类型限制实例 Associated Types in Action
下面定义了一个没有使用泛型的函数:
func findStringIndex(array: [String], valueToFind: String) -> Int? {
for (index, value) in enumerate(array) {
if value == valueToFind {
return index
}
}
return nil
}
使用上面的函数:
let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
if let foundIndex = findStringIndex(strings, "llama") {
println("The index of llama is \(foundIndex)")
}
// prints "The index of llama is 2"
下面定义泛型版本:
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标准库提供了 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 }
泛型参数写作 T: Equatable 表示“T 必须是实现了
Equatable
协议的类型”。
findIndex
函数限制编译成功,可以调用了:
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
9. 关联类型 Associated Types
当定义协议时,可以将 Associated Types定义为协议的一部分。Associated Types 使用一个占位符(或者叫 alias)代表一种在协议中使用的类型,协议中不指定 Associated Types 的实际类型,直到实现了协议的类型中才确定 Associated Types 的实际类型。使用 typealias
关键字定义关联类型
定义一个协议Container
:
protocol Container {
typealias ItemType
mutating func append(item: ItemType)
var count: Int { get }
subscript(i: Int) -> ItemType { get }
}
下面定义了一个没有使用泛型的 IntStack
结构体:
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] } }
ItemType 的类型在 IntStack 中被确定。在IntStack中,代码 typealias ItemType = Int
将ItemType 由协议中的抽象类型转换为具体的 Int类型。
实际上,编译器可以根据IntStack结构体中的append函数参数或者subscript的返回值推断出ItemType的类型,因此 typealias ItemType = Int
这句代码可以省略不写。
下面使用泛型实现上面的功能:
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] } }
根据append的参数和subscript的返回值,编译器可以推断出 ItemType 的类型为泛型参数 T。
10. 给已存在的类型扩展一个关联类型 Extending an Existing Type to Specify an Associated Type
Swift的数组Array已经提供了一个append方法,一个count属性和一个含有一个Int参数的下标表达式,这三点完全满足了 Container协议,这意味着你可以扩展Array,让它实现Container协议:
extension Array: Container {}
Array的append方法和下标表达式可以推断出ItemType的类型,这一点与上面的泛型Stack是一样的。
11. where子句 Where Clauses
上面介绍了泛型的类型限制,同样也可以对关联类型进行限制。
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 }
上面函数的泛型参数type parameter需要满足下面要求:
1)C1 要实现 Container
协议。(C1: Container
)
2)C2 要实现 Container
协议。(C2: Container
)
3)C1的ItemType必须与C2的ItemType相同。(C1.ItemType == C2.ItemType
)
4)C1的ItemType必须实现 Equatable
协议。(C1.ItemType: Equatable
)
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.") } // prints "All items match."
即使stackOfStrings和arrayOfStrings是不同的类型,但是他们实现了Container
协议,含有相同类型的值,所以可以将它们作为 allItemsMatch 的参数。