泛型代码 能够根据自定义的需求,编写出适用于任意类型、灵活可重用的函数及类型。避免代码的重复,用一种清晰和抽象的方式来表达代码的意图。
Swift 的 Array
和 Dictionary
都是泛型集合。你可以创建一个 Int
数组,也可创建一个 String
数组,甚至可以是任意其他 Swift 类型的数组。同样的,你也可以创建存储任意指定类型的字典。
泛型所解决的问题
下面是一个标准的非泛型函数 swapTwoInts(_:_:)
,用来交换两个 Int
值:
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
}
在实际应用中,通常需要一个更实用更灵活的函数来交换两个任意类型的值,幸运的是,泛型代码帮你解决了这种问题。(这些函数的泛型版本已经在下面定义好了。)
注意
在上面三个函数中,a
和b
类型必须相同。如果a
和b
类型不同,那它们俩就不能互换值。Swift 是类型安全的语言,所以它不允许一个String
类型的变量和一个Double
类型的变量互换值。试图这样做将导致编译错误。
泛型函数
泛型函数可以适用于任何类型,下面的 swapTwoValues(_:_:)
函数是上面三个函数的泛型版本:
func swapTwoValues(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}
泛型函数名后面跟着占位类型名(这里用字母 T
来表示),并用尖括号括起来(
)。占位类型名没指明必须是什么类型,但指明了 a
和 b
必须是同一类型 T
, 函数在调用时才会根据所传入的实际类型决定 T
所代表的类型。
var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt 现在 107, and anotherInt 现在 3
var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString 现在 "world", and anotherString 现在 "hello"
注意
上面定义的swapTwoValues(_:_:)
函数是受swap(_:_:)
函数启发而实现的。后者存在于 Swift 标准库,你可以在你的应用程序中使用它。如果你在代码中需要类似swapTwoValues(_:_:)
函数的功能,你可以使用已存在的swap(_:_:)
函数。
类型参数
在上面的 swapTwoValues(_:_:)
例子中,占位类型 T
是类型参数的一个例子。类型参数指定并命名一个占位类型,并且紧随在函数名后面,使用一对尖括号括起来(例如
)。
一旦一个类型参数被指定,你可以用它来定义一个函数的参数类型(例如 swapTwoValues(_:_:)
函数中的参数 a
和 b
),或者作为函数的返回类型,还可以用作函数主体中的注释类型。在这些情况下,类型参数会在函数调用时被实际类型所替换。
你可提供多个类型参数,将它们都写在尖括号中,用逗号分开。
命名类型参数
在大多数情况下,类型参数具有一个描述性名字,例如 Dictionary
中的 Key
和 Value
,以及 Array
中的 Element
,这可以告诉阅读代码的人这些类型参数和泛型函数之间的关系。然而,当它们之间没有有意义的关系时,通常使用单个字母来命名,例如 T
、U
、V
,正如上面演示的 swapTwoValues(_:_:)
函数中的 T
一样。
注意
请始终使用大写字母开头的驼峰命名法(例如T
和MyTypeParameter
)来为类型参数命名,以表明它们是占位类型,而不是一个值。
泛型类型
除了泛型函数,Swift 还允许你定义泛型类型。这些自定义类、结构体和枚举可以适用于任何类型,类似于 Array
和 Dictionary
。
这部分内容将向你展示如何编写一个名为 Stack
(栈)的泛型集合类型。栈是一系列值的有序集合,和 Array
类似,但它相比 Swift 的 Array
类型有更多的操作限制。数组允许在数组的任意位置插入新元素或是删除其中任意位置的元素。而栈只允许在集合的末端添加新的元素(称之为入栈)。类似的,栈也只能从末端移除元素(称之为出栈)。
注意
栈的概念已被UINavigationController
类用来构造视图控制器的导航结构。你通过调用UINavigationController
的pushViewController(_:animated:)
方法来添加新的视图控制器到导航栈,通过popViewControllerAnimated(_:)
方法来从导航栈中移除视图控制器。每当你需要一个严格的“后进先出”方式来管理集合,栈都是最实用的模型。
下面展示了如何编写一个非泛型版本的栈,以 Int
型的栈为例:
struct IntStack {
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
}
上面的 IntStack
结构体只能用于 Int
类型。不过,可以定义一个泛型 Stack
结构体,从而能够处理任意类型的值。
下面是相同代码的泛型版本:
struct Stack {
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
例如,要创建一个 String
类型的栈,可以写成 Stack
:
var stackOfStrings = Stack()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// 栈中现在有 4 个字符串
移除并返回栈顶部的值 "cuatro"
,即将其出栈:
let fromTheTop = stackOfStrings.pop()
// fromTheTop 的值为 "cuatro",现在栈中还有 3 个字符串
扩展一个泛型类型
当你扩展一个泛型类型的时候,你并不需要在扩展的定义中提供类型参数列表。原始类型定义中声明的类型参数列表在扩展中可以直接使用,并且这些来自原始类型中的参数名称会被用作原始定义中类型参数的引用。
下面的例子扩展了泛型类型 Stack
,为其添加了一个名为 topItem
的只读计算型属性,它将会返回当前栈顶端的元素而不会将其从栈中移除:
extension Stack {
var topItem: Element? {
return items.isEmpty ? nil : items[items.count - 1]
}
}
类型约束
swapTwoValues(_:_:)
函数和 Stack
类型可以作用于任何类型。需要时,类型约束可以指定一个类型参数必须继承自指定类,或者符合一个特定的协议或协议组合。
例如,Swift 的 Dictionary
类型对字典的键的类型做了些限制。字典的键的类型必须是可哈希(hashable
)的。这是为了便于检查字典是否已经包含某个特定键的值。字典的键类型上,要求必须符合 Hashable
协议。所有的 Swift 基本类型(例如 String
、Int
、Double
和 Bool
)默认都是可哈希的。
类型约束语法
你可以在一个类型参数名后面放置一个类名或者协议名,并用冒号进行分隔,来定义类型约束,它们将成为类型参数列表的一部分。对泛型函数添加类型约束的基本语法如下所示(作用于泛型类型时的语法与之相同):
func someFunction(someT: T, someU: U) {
// 这里是泛型函数的函数体部分
}
上面这个函数有两个类型参数。第一个类型参数 T
,有一个要求 T
必须是 SomeClass
子类的类型约束;第二个类型参数 U
,有一个要求 U
必须符合 SomeProtocol
协议的类型约束。
类型约束实践
下面非泛型函数,功能是在一个 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
}
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”
findIndex(ofString:in:)
函数的泛型版本 。
func findIndex(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
类型约束作为类型参数定义的一部分:
func findIndex(of valueToFind: T, in array:[T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
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
关联类型
定义一个协议时,有的时候声明一个或多个关联类型作为协议定义的一部分将会非常有用。关联类型为协议中的某个类型提供了一个占位名(或者说别名),其代表的实际类型在协议被采纳时才会被指定。你可以通过 associatedtype
关键字来指定关联类型。
关联类型实践
下面例子定义了一个 Container
协议,该协议定义了一个关联类型 ItemType
:
protocol Container {
associatedtype ItemType
mutating func append(_ item: ItemType) //添加一个新元素到容器里
var count: Int { get } //获取容器中元素的数量
subscript(i: Int) -> ItemType { get } //通过 下标检索对应元素
}
下面是先前的非泛型的 IntStack
类型,这一版本采纳并符合了 Container
协议:
struct IntStack: Container {
// IntStack 的原始实现部分
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
// Container 协议的实现部分
typealias ItemType = Int
mutating func append(_ item: Int) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Int {
return items[i]
}
}
你也可以让泛型 Stack
结构体遵从 Container
协议:
struct Stack: Container {
// Stack 的原始实现部分
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
// Container 协议的实现部分
mutating func append(_ item: Element) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Element {
return items[i]
}
}
Swift 的类型推断,占位类型参数 Element
被用作 append(_:)
方法的 item
参数和下标的返回类型。可以推断出 Element
的类型即是 ItemType
的类型。
通过扩展一个存在的类型来指定关联类型
“通过扩展添加协议一致性” 中描述了如何利用扩展让一个已存在的类型符合一个协议,这包括使用了关联类型的协议。
Swift 的 Array
类型已经提供 append(_:)
方法,一个 count
属性,以及一个接受 Int
类型索引值的下标用以检索其元素。这三个功能都符合 Container
协议的要求,也就意味着你只需简单地声明 Array
采纳该协议就可以扩展 Array
,使其遵从 Container
协议。你可以通过一个空扩展来实现这点,正如“通过扩展采纳协议”中的描述:
extension Array: Container {}
如同上面的泛型 Stack
结构体一样,Array
的 append(_:)
方法和下标确保了 Swift 可以推断出 ItemType
的类型。定义了这个扩展后,你可以将任意 Array
当作 Container
来使用。
约束关联类型
你可以给协议里的关联类型添加类型注释,让遵守协议的类型必须遵循这个约束条件。
例如,下面的代码定义了一个 Item
必须遵循 Equatable
的 Container
类型:
protocol Container {
associatedtype Item: Equatable
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
为了遵守了 Container
协议,Item 类型也必须遵守 Equatable
协议。
泛型 where 语句
“类型约束” 让你能够为泛型函数,下标,类型的类型参数定义一些强制要求。
可以在参数列表中通过 where
子句为关联类型定义约束。通过 where
子句要求一个关联类型遵从某个特定的协议,以及某个特定的类型参数和关联类型必须类型相同。你可以通过将 where
关键字紧跟在类型参数列表后面来定义 where
子句,where
子句后跟一个或者多个针对关联类型的约束,以及一个或多个类型参数和关联类型间的相等关系。你可以在函数体或者类型的大括号之前添加 where 子句。
检查两个 Container
实例是否包含相同顺序的相同元素,被检查的两个 Container
可以不是相同类型的容器(虽然它们可以相同),但它们必须拥有相同类型的元素。这个要求通过一个类型约束以及一个 where
子句来表示:
func allItemsMatch
(_ someContainer: C1, _ anotherContainer: C2) -> Bool
where C1.ItemType == C2.ItemType, C1.ItemType: Equatable {
// 检查两个容器含有相同数量的元素
if someContainer.count != anotherContainer.count {
return false
}
// 检查每一对元素是否相等
for i in 0..
这个函数的类型参数列表定义了对两个类型参数的要求:
-
C1
必须符合Container
协议(写作C1: Container
)。 -
C2
必须符合Container
协议(写作C2: Container
)。 -
C1
的ItemType
必须和C2
的ItemType
类型相同(写作C1.ItemType == C2.ItemType
)。 -
C1
的ItemType
必须符合Equatable
协议(写作C1.ItemType: Equatable
)。
使用:
var stackOfStrings = Stack()
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.")
}
// 打印 “All items match.”
具有泛型 where 子句的扩展
你也可以使用泛型 where
子句作为扩展的一部分。基于以前的例子,下面的示例扩展了泛型 Stack
结构体,添加一个 isTop(_:)
方法。
extension Stack where Element: Equatable {
func isTop(_ item: Element) -> Bool {
//检查这个栈是不是空的
guard let topItem = items.last else {
return false
}
return topItem == item
}
}
如果尝试在其元素不符合 Equatable 协议的栈上调用 isTop(_:)
方法,则会收到编译时错误。
struct NotEquatable { }
var notEquatableStack = Stack()
let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)
notEquatableStack.isTop(notEquatableValue) // 报错
你可以使用泛型 where
子句去扩展一个协议。基于以前的示例,下面的示例扩展了 Container
协议,添加一个 startsWith(_:)
方法。
extension Container where Item: Equatable {
func startsWith(_ item: Item) -> Bool {
return count >= 1 && self[0] == item
}
}
if [9, 9, 9].startsWith(42) {
print("Starts with 42.")
} else {
print("Starts with something else.")
}
// 打印 "Starts with something else."
上述示例中的泛型 where
子句要求 Item
符合协议,但也可以编写一个泛型 where
子句去要求 Item
为特定类型。例如:
extension Container where Item == Double {
func average() -> Double {
var sum = 0.0
for index in 0..
具有泛型 where 子句的关联类型
你可以在关联类型后面加上具有泛型 where
的字句。例如,建立一个包含迭代器(Iterator)的容器,就像是标准库中使用的 Sequence
协议那样。你应该这么写:
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
associatedtype Iterator: IteratorProtocol where Iterator.Element == Item
func makeIterator() -> Iterator
}
迭代器(Iterator)的泛型 where
子句要求:无论迭代器是什么类型,迭代器中的元素类型,必须和容器项目的类型保持一致。makeIterator()
则提供了容器的迭代器的访问接口。
一个协议继承了另一个协议,你通过在协议声明的时候,包含泛型 where
子句,来添加了一个约束到被继承协议的关联类型。例如,下面的代码声明了一个 ComparableContainer
协议,它要求所有的 Item
必须是 Comparable
的。
protocol ComparableContainer: Container where Item: Comparable { }
泛型下标
下标能够是泛型的,他们能够包含泛型 where
子句。你可以把占位符类型的名称写在 subscript
后面的尖括号里,在下标代码体开始的标志的花括号之前写下泛型 where
子句。例如:
extension Container {
subscript(indices: Indices) -> [Item]
where Indices.Iterator.Element == Int {
var result = [Item]()
for index in indices {
result.append(self[index])
}
return result
}
}
这个 Container
协议的扩展添加了一个下标方法,接收一个索引的集合,返回每一个索引所在的值的数组。这个泛型下标的约束如下:
这个 Container
协议的扩展添加了一个下标:下标是一个序列的索引,返回的则是索引所在的项目的值所构成的数组。这个泛型下标的约束如下:
- 在尖括号中的泛型参数
Indices
,必须是符合标准库中的Sequence
协议的类型。 - 下标使用的单一的参数,
indices
,必须是Indices
的实例。 - 泛型
where
子句要求 Sequence(Indices)的迭代器,其所有的元素都是Int
类型。这样就能确保在序列(Sequence)中的索引和容器(Container)里面的索引类型是一致的。
综合一下,这些约束意味着,传入到 indices
下标,是一个整型的序列。
(译者:原来的 Container
协议,subscript
必须是 Int
型的,扩展中新的 subscript
,允许下标是一个的序列,而非单一的值。)