泛型代码让你能根据自定义的需求,编写出适用于任意类型的、灵活可复用的函数及类型。你可避免编写重复的代码,⽽是用一
种清晰抽象的方式来表达代码的意图。
泛型是 Swift 最强大的特性之一,很多 Swift 标准库是基于泛型代码构建的。实际上,即使你没有意识到,你也一直在语言指南
中使用泛型。例如,Swift 的 Array 和 Dictionary 都是泛型集合。你可以创建一个 Int 类型数组,也可创建一个 String 类型数
组,甚⾄可以是任意其他 Swift 类型的数组。同样,你也可以创建一个存储任意指定类型的字典,并对该类型没有限制。
泛型解决的问题
下⾯是一个标准的非泛型函数 swapTwoInts(_:_:) ,⽤来交换两个 Int 值:
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a=b
b = temporaryA
}
这个函数使用输入输出参数 ( inout ) 来交换 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
}
你可能注意到了, swapTwoInts(_:_:‘) 、 swapTwoStrings(_:_:) 和 swapTwoDoubles(_:_:) 函数体是一样的,唯一的区别是它们接
收的参数类型( Int 、 String 和 Double )。
在实际应用中,通常需要一个更实用更灵活的函数来交换两个任意类型的值,幸运的是,泛型代码帮你解决了这种问题。 (这些
函数的泛型版本已经在下面定义好了。)
注意
在上面三个函数中, a 和 b 类型必须相同。如果 a 和 b 类型不同,那它们俩就不能互换值。Swift 是类型安全的语言,所以它不
允许一个 String 类型的变量和一个 Double 类型的变量互换值。试图这样做将导致编译错误。
泛型函数
泛型函数可适用于任意类型,下面是函数 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 具体的类型,但它要求 a 和 b 必须是相同的类型, T 的实际类型由每次调用swapTwoValues(_:_:) 来决定。
泛型函数和非泛型函数的另外一个不同之处在于这个泛型函数名( swapTwoValues(_:_:) )后面跟着占位类型名( T ),并用尖括号
括起来( <T> )。这个尖括号告诉 Swift 那个 T 是 swapTwoValues(_:_:) 函数定义内的一 个占位类型名,因此 Swift 不会去查找
名为 T 的实际类型。
swapTwoValues(_:_:) 函数现在可以像 swapTwoInts(_:_:) 那样调用,不同的是它能接受两个任意类型的值,条件是这两个值有着
相同的类型。 swapTwoValues(_:_:) 函数被调用时, T 所代表的类型都会由传入的值的类型推断出来。
在下⾯的两个例子中, T 分别代表 Int 和 String :
var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt 现在是 107,anotherInt 现在是 3
var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString 现在是“world”,anotherString 现在是“hello”
注意
上⾯定义的 swapTwoValues(_:_:) 函数是受 swap(_:_:) 函数启发⽽实现的。后者存在于 Swift 标准库,你可以在你的应用程序中
使用它。如果你在代码中需要类似 swapTwoValues(_:_:) 函数的功能,你可以使用已存在的 swap(_:_:) 函数。
类型参数
上面 swapTwoValues(_:_:) 例子中,占位类型 T 是一个类型参数的例子,类型参数指定并命名一个占位类型,并且紧随在函数名
后面,使用一对尖括号括起来(例如 <T> )。
一旦一个类型参数被指定,你可以用它来定义一个函数的参数类型(例如 swapTwoValues(_:_:) 函数中的参数 a 和 b ),或者作为函
数的返回类型,还可以用作函数主体中的注释类型。在这些情况下,类型参数会在函数调用时被实际类型所替换。(在上面的
swapTwoValues(_:_:) 例子中,当函数第一次被调用时, T 被 Int 替换,第二次调用时,被 String 替换。) 你可提供多个类型参
数,将它们都写在尖括号中,用逗号分开。
命名类型参数
⼤多情况下,类型参数具有描述下的名称,例如字典 Dictionary<Key, Value> 中的 Key 和 Value 及数组 Array<Element> 中的
Element ,这能告诉阅读代码的人这些参数类型与泛型类型或函数之间的关系。然而,当它们之间没有有意义的关系时,通常使
用单个字符来表示,例如 T 、 U 、 V ,例如上面演示函数 swapTwoValues(_:_:) 中的 T 。
注意
请始终使用大写字母开头的驼峰命名法(例如 T 和 MyTypeParameter )来为类型参数命名,以表明它们是占位类型,⽽不是一个
值。
泛型类型
除了泛型函数,Swift 还允许自定义泛型类型。这些自定义类、结构体和枚举可以适用于任意类型,类似于 Array 和Dictionary 。
本节将向你展示如何编写一个名为 Stack (栈)的泛型集合类型。栈是值的有序集合,和数组类似,但比数组有更严格的操作限
制。数组允许在其中任意位置插入或是删除元素。而栈只允许在集合的末端添加新的元素(称之为入栈)。类似的,栈也只能从末
端移除元素(称之为出栈)。
注意
栈的概念已被 UINavigationController 类用来构造视图控制器的导航结构。你通过调用 UINavigationController 的
pushViewController(_:animated:) ⽅法来添加新的视图控制器到导航栈,通过 popViewControllerAnimated(_:) 方法来从导航栈中
移除视图控制器。每当你需要一个严格的“后进先出”方式来管理集合,栈都是最实用的模型。
下图展示了入栈(push)和出栈(pop)的行为:
1. 现在有三个值在栈中。
2. 第四个值被压入到栈的顶部。
3. 现在栈中有四个值,最近入栈的那个值在顶部。
4. 栈中最顶部的那个值被移除出栈。
5. 一个值移除出栈后,现在栈又只有三个值了。
下⾯展示如何编写一个非泛型版本的栈,以 Int 型的栈为例:
struct IntStack {
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
}
这个结构体在栈中使用一个名为 items 的数组属性来存储值。栈提供了两个方法: push(_:) 和 pop() ,用来向栈中压入值以及从栈
中移除值。这些方法被标记为 mutating ,因为它们需要修改结构体的 items 数组。
上面的 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 基本上和 IntStack 相同,只是用占位类型参数 Element 代替了实际的 Int 类型。这个类型参数包裹在紧随结构体名
的一对尖括号里(< Element >)。
Element 为待提供的类型定义了一个占位名。这种待提供的类型可以在结构体的定义中通过 Element 来引用。在这个例子中,
Element 在如下三个地⽅被用作占位符:
1.创建 items 属性,使用 Element 类型的空数组对其进⾏初始化。
2.指定 push(_:) 方法的唯一参数 item 的类型必须是 Element 类型。
3.指定 pop() 方法的返回值类型必须是 Element 类型。
由于 Stack 是泛型类型,因此可以用来创建适用于 Swift 中任意有效类型的栈,就像 Array 和 Dictionary 那样。
你可以通过在尖括号中写出栈中需要存储的数据类型来创建并初始化一个 Stack 实例。例如,要创建⼀个 String 类型的栈,可以
写成 Stack<String>() :
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// 栈中现在有 4 个字符串
下图展示了 stackOfStrings 如何将这四个值压栈:
移除并返回栈顶部的值“cuatro”,即出栈:
let fromTheTop = stackOfStrings.pop()
// fromTheTop 的值为“cuatro”,现在栈中还有 3 个字符串
下图展示了如何将顶部的值出栈:
泛型扩展
当对泛型类型进⾏扩展时,你并不需要提供类型参数列表作为定义的一部分。原始类型定义中声明的类型参数列表在扩展中可以
直接使用,并且这些来自原始类型中的参数名称会被用作原始定义中类型参数的引用。
下⾯的例子扩展了泛型类型 Stack ,为其添加了一个名为 topItem 的只读计算型属性,它将会返回当前栈顶元素且不会将其从栈
中移除:
extension Stack {
var topItem: Element? {
return items.isEmpty ? nil : items[items.count - 1]
}
}
topItem 属性会返回 Element 类型的可选值。当栈为空的时候, topItem 会返回 nil ;当栈不为空的时候, topItem 会返回 items
数组中的最后一个元素。
注意:这个扩展并没有定义类型参数列表。相反的, Stack 类型已有的类型参数名称 Element ,被用在扩展中来表示计算型属性
topItem 的可选类型。
计算型属性 topItem 现在可以用来访问任意 Stack 实例的顶端元素且不移除它:
if let topItem = stackOfStrings.topItem {
print("The top item on the stack is \(topItem).")
}
// 打印“The top item on the stack is tres.”
泛型类型的扩展,还可以包括类型扩展需要额外满足的条件,从而对类型添加新功能,这一部分将在《具有泛型 Where 子句的
扩展》中进⾏讨论。
类型约束
swapTwoValues(_:_:) 函数和 Stack 适用于任意类型。不过,如果能对泛型函数或泛型类型中添加特定的类型约束,这将在某些情
况下非常有用。类型约束指定类型参数必须继承自指定类、遵循特定的协议或协议组合。
例如,Swift 的 Dictionary 类型对字典的键的类型做了些限制。在字典的描述中,字典键的类型必须是可哈希 (hashable)的。也
就是说,必须有一种方法能够唯一地表示它。字典键之所以要是可哈希的,是为了便于检查字典中是否已经包含某个特定键的
值。若没有这个要求,字典将无法判断是否可以插入或替换某个指定键的值,也不能查找到已经存储在字典中的指定键的值。
这个要求通过 Dictionary 键类型上的类型约束实现,它指明了键必须遵循 Swift 标准库中定义的 Hashable 协议。
所有 Swift 的基本类型(例如 String 、 Int 、 Double 和 Bool )默认都是可哈希的。
当自定义泛型类型时,你可以定义你自己的类型约束,这些约束将提供更为强大的泛型编程能⼒。像可哈希 (hashable) 这种抽
象概念根据它们的概念特征来描述类型,而不是它们的具体类型。
类型约束语法
在一个类型参数名后面放置一个类名或者协议名,并用冒号进⾏分隔,来定义类型约束。下面将展示泛型函数约束的基本语法(与
泛型类型的语法相同):
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// 这⾥是泛型函数的函数体部分
}
上面这个函数有两个类型参数。第一个类型参数 T 必须是 SomeClass 子类;第二个类型参数 U 必须符合SomeProtocol 协议。
类型约束实践
这⾥有个名为 findIndex(ofString:in:) 的非泛型函数,该函数的功能是在一个 String 数组中查找给定 String 值的索引。若查找到
匹配的字符串, findIndex(ofString:in:) 函数返回该字符串在数组中的索引值,否则返回 nil :
func findIndex(ofString valueToFind: String, in array: [String]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
findIndex(ofString:in:) 函数可以用于查找字符串数组中的某个字符串值:
let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
if let foundIndex = findIndex(ofString: "llama", in: strings) {
print("The index of llama is \(foundIndex)")
}
// 打印“The index of llama is 2”
如果只能查找字符串在数组中的索引,用处不是很大。不过,你可以用占位类型 T 替换 String 类型来写出具有相同功能的泛型函
数 findIndex(_:_:) 。
下⾯展示了 findIndex(ofString:in:) 函数的泛型版本 findIndex(of:in:) 。请注意这个函数返回值的类型仍然 是 Int? ,这是因为函数
返回的是一个可选的索引数,⽽不是从数组中得到的一个可选值。需要提醒的是,这个函数无法通过编译,原因将在后面说明:
func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
上⾯所写的函数无法通过编译。问题出在相等性检查上,即 " if value == valueToFind "。不是所有的 Swift 类型都可以用等式符(
== )进⾏比较。例如,如果你自定义类或结构体来描述复杂的数据模型,对于这个类或结构体而言, Swift 无法明确知道“相
等”意味着什么。正因如此,这部分代码无法保证适用于任意类型 T ,当你试图编译这部分代码时就会出现相应的错误。
不过,所有的这些并不会让我们无从下手。Swift 标准库中定义了一个 Equatable 协议,该协议要求任何遵循该协议的类型必须
实现等式符( == )及不等符( != ),从而能对该类型的任意两个值进⾏比较。所有的 Swift 标准类型自动支持 Equatable 协议。
遵循 Equatable 协议的类型都可以安全地用于 findIndex(of:in:) 函数,因为其保证支持等式操作符。为了说明这个事情,当定义
一个函数时,你可以定义一个 Equatable 类型约束作为类型参数定义的一部分:
func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
findIndex(of:in:) 类型参数写做 T: Equatable ,也就意味着“任何符合 Equatable 协议的类型 T ”。
findIndex(of:in:) 函数现在可以成功编译了,并且适用于任何符合 Equatable 的类型,如 Double 或 String :
let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])
// doubleIndex 类型为 Int?,其值为 nil,因为 9.3 不不在数组中
let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"])
// stringIndex 类型为 Int?,其值为 2