第二十二章 泛型
泛型代码可以是我们写出灵活的和可重复使用的适用于任何类型的函数(function)或类型(type)
我们可以在写代码的时候避免代码的重复和以一个简洁明了和抽象的人的方式来表达想要实现的代码。
泛型是Swift语言最强大的一个特性,并且许许多多Swift标准库都使用泛型来构建的。 事实上,泛型贯穿了整个swift语法指导书,只是你没有意识到。 举个例子,swift中的数组和字典类型其实都是泛型集合(generic collection),创建一个Int
值的数组,或着一个String值的数组。就像这样可以创建一个任意类型。相同的道理创建字典的时候也是一样的。
1. The Problem That Generics Solve (泛型解决的问题)
下面是一个标准的非泛型的函数swapTwoInts(_:_:)
用来调换两个Int
的值。
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
这个函数使用的是一个输入输出型参数来调换a和b的值,输入输出型参数相见前面几章。这个swapTwoInts(_:_:)
函数把b的原址调换成a的值,把a的原值调换成b的值,所以我们可以称之为调换两个Int
变量的值。
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// 输出:someInt is now 107, and anotherInt is now 3
虽然这个swapTwoInts(_:_:)
的函数非常实用,但是仅限于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
}
你可能也注意到了这两个函数的函数体是完全一致的,唯一不同的是它们所接受值的类型是不同的(String,Int和Double),在实际应用中,通常需要一个更实用更灵活的函数来交换两个任意类型的值,幸运的是,泛型代码帮你解决了这 种问题。(这些函数的泛型版本已经在下面定义好了。)
2. Generic Functions (泛型函数)
泛型函数适用于任何类型,下面的例子就是swapTwoInts(_:_:)
的泛型版本,叫做swapTwoValues(_:_:)
// 泛型版本 调换两个值
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}
函数swapTwoValues(_:_:)
的函数体和swapTwoInts(_:_:)
的函数体是一样的,然而有一点不同的就是两个不同函数代码的第一行。下面是这两个代码的第一行对比。
func swapTwoInts(_ a: inout Int, _ b: inout Int)
func swapTwoValues<T>(_ a: inout T, _ b: inout T)
泛型版本的这个函数使用的是一个占位符(叫做T在这个案例中),而不是实际的类型名字(Int,String或Double),占位符的名字并不能说明这个T必须是什么什么,但这个占位符T指明了这个a和b的值必须要是同一个类型。无论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"
3. Type Parameters (类型参数)
在上面这个函数swapTwoValues(_:_:)
案例中,占位类型T是类型参数的一个例子,类型参数知名和命名一个占位类型。并且会立即写在在函数名的后面。在把这个占位类型包含在一堆尖括号里面()
一旦我们指明了一个类型参数,就可以使用它来定义一个函数参数的类型(函数swapTwoValues(_:_:)
的参数a和b)或者是函数的返回类型,在或者是函数体里面的类型注释,在每一个例子里面,无论什么时候调用函数,类型参数总是用一个实际的类型所替代。(在上面swapTwoValues(_:_:)
的案例里,T在第一次函数被调用的时候被Int
所替代,第二次被调用的时候被String
所替代),也可以在尖括号内通过用逗号隔开多个类型参数名的方法来提供多个类型参数。
4. Naming Type Parameters (命名类型参数)
在大多是情况下,类型参数有一个描述性的名称,就像字典里面的健与值Dictionary<Key, Value>
和数组中的元素Array<Element>
,这种表达是用来告诉读者函数的类型参数和泛型类型之间的关系。然而如果说类型参数和泛型类型之间没有什么有意义的关系,都会用单一的字母 T
,U
,V
5. Generic Types (泛型类型)
除过泛型函数之外呢,swift还可以让我们自定义泛型类型,这些自定义的泛型类型包含了自定义的类,结构体和枚举,它们都适用于任何类型,类似于数组和字典。
下面这个章节向我们展示了 如何写泛型集合类型Stack。栈(stack
)是一个有序的值的集合。和数组(array
)较为相似。数组是一个无序的值的集合,数组可以在其位置的任何一个地方允许任何一个元素被移除或插入。然而,栈只能允许在记恨的底部添加新的元素(入栈),类似的栈也只能允许元素从集合的底部被移出(出栈),
下面这个图例想我们展示说明了入栈(push
)和出栈(pop
)的整个过程。
- 当前栈的集合里面有三个值 ( 第一步 )
- 第四个值添加到了栈的最顶部 ( 第二步 )
- 现在栈里面有四个值,最新的值在栈的最顶端 ( 第三步 )
- 最顶端的值被移除出栈 ( 第四步 )
- 移除完之后,现在栈里面又有三个值了 ( 第五步 )
下面是如何写一个非泛型版的栈(stack)。使用的是Int
类型的栈
struct IntStack {
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
}
该结构体使用的是一个数组结构的属性来在栈里面存储这些值的。栈提供了两种方法push
和pop
。用来添加和移除栈里面的值。这些方法被标记为mutating
,所以可能会修改结构体里数组里面的元素。
上面的IntStack
类型只能用于Int
值,然而,可以把它定义为泛型的Stack类 这样可以管理栈里面任何类型的值。下面是相同代码的泛型版本。
struct Stack<Element> {
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
现在这个泛型版本的Stack
和非泛型版本的Stack基本上相同了。只有把实际的类型Int
替换成了类型参数Element
,该类型参数直接写在结构体的名字之后并被包含在一对尖括号内了。
而,这个Element
其实被定义成了一个占位符的名称,为后来提供某一个类型做准备。后面的类型可以结构体的定义里面作为Element
来参考引用。在这个例子里面 Element 在三个地方被用作占位符。
- 创建了一个名为items的属性,是由一个类型为Element的空白值的数组。
- 指明了这个
push(_:)
方法有一个类型为Element的单一的参数item。 - 指明了这个
pop()
方法返回的值必须也得是类型为Element的值。
因为它是一个泛型类型,Stack可以被用来创建一个任何有效的类型的栈,就像数组或字典那样。可以通过在尖括号中写出栈中需要存储的数据类型来创建并初始化一个Stack实例。例如,要创建一个String类型的栈,可以写成Stack<String>()
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// the stack now contains 4 strings
下面就是这个实例怎么在栈里面添加(push)四个值的过程示例图。
移除栈最顶端的那个值 “cuatro” 示例图和代码如下。
let fromTheTop = stackOfStrings.pop()
// fromTheTop is equal to "cuatro", and the stack now contains 3 strings