func swapTwoInts(_ a: inout Int, _ b: inout Int) { // 这个函数使用输入输出参数(inout)来交换 a 和 b 的值
let temporaryA = a
a = b
b = temporaryA
}
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”
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 类型的变量互换值。
试图这样做将导致编译错误。
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( _ : _ : ) 来决定。
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( _ : _ : ) 函数。
注意:
请始终使用大写字母开头的驼峰命名法(例如 T 和 MyTypeParameter)来为类型参数命名,以表明它们是占位类型,而不是一个值。
注意
栈的概念已被 UINavigationController 类用来构造视图控制器的导航结构。
你通过调用 UINavigationController 的 pushViewController(:animated:) 方法来添加新的视图控制器到导航栈,
通过 popViewControllerAnimated(? 方法来从导航栈中移除视图控制器。
每当你需要一个严格的“后进先出”方式来管理集合,栈都是最实用的模型。
入栈(push)和出栈(pop)的行为:
下面展示如何编写一个非泛型版本的栈,以 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 数组。
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 类型。这个类型参数包裹在紧随结构体名的一对尖括号里()。
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// 栈中现在有 4 个字符串
let fromTheTop = stackOfStrings.pop()
// fromTheTop 的值为“cuatro”,现在栈中还有 3 个字符串
当对泛型类型进行扩展时,你并不需要提供类型参数列表作为定义的一部分。
原始类型定义中声明的类型参数列表在扩展中可以直接使用,并且这些来自原始类型中的参数名称会被用作原始定义中类型参数的引用。
下面的例子扩展了泛型类型 Stack,为其添加了一个名为 topItem 的只读计算型属性,它将会返回当前栈顶元素且不会将其从栈中移除:
extension Stack {
var topItem: Element? {
return items.isEmpty ? nil : items[items.count - 1]
}
}
注意:
1. 这个扩展并没有定义类型参数列表。
2. 相反的,Stack 类型已有的类型参数名称 Element,被用在扩展中来表示计算型属性 topItem 的可选类型。
if let topItem = stackOfStrings.topItem {
print("The top item on the stack is \(topItem).")
}
// 打印“The top item on the stack is tres.”
泛型类型的扩展,还可以包括类型扩展需要额外满足的条件,从而对类型添加新功能,这一部分将在具有泛型 Where 子句的扩展中进行讨论。
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// 这里是泛型函数的函数体部分
}
上面这个函数有两个类型参数。
第一个类型参数 T 必须是 SomeClass 子类;
第二个类型参数 U 必须符合 SomeProtocol 协议。
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”
如果只能查找字符串在数组中的索引,用处不是很大。
不过,你可以用占位类型 T 替换 String 类型来写出具有相同功能的泛型函数 findIndex( _ : _ : )。
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 协议。
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”。
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
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
- 该协议没有指定容器中元素该如何存储以及元素类型。
- 该协议只指定了任何遵从 Container 协议的类型必须提供的三个功能。遵从协议的类型在满足这三个条件的情况下,也可以提供其他额外的功能。
- 任何遵从 Container 协议的类型必须能够指定其存储的元素的类型。具体来说,它必须确保添加到容器内的元素以及下标返回的元素类型是正确的。
为了定义这些条件,Container 协议需要在不知道容器中元素的具体类型的情况下引用这种类型。
Container 协议需要指定任何通过 append(_? 方法添加到容器中的元素和容器内的元素是相同类型,并且通过容器下标返回的元素的类型也是这种类型。
为此,Container 协议声明了一个关联类型 Item,写作 associatedtype Item。 协议没有定义 Item 是什么,这个信息留给遵从协议的类型来提供。
尽管如此,Item 别名提供了一种方式来引用 Container 中元素的类型,并将之用于 append( _ : ) 方法和下标,从而保证任何 Container 的行为都能如预期。
这是前面非泛型版本 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 Item = Int
mutating func append(_ item: Int) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Int {
return items[i]
}
}
IntStack 在实现 Container 的要求时,指定 Item 为 Int 类型,即 typealias Item = Int,从而将 Container 协议中抽象的 Item 类型转换为具体的 Int 类型。
struct Stack<Element>: 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]
}
}
占位类型参数 Element 被用作 append( _ : ) 方法的 item 参数和下标的返回类型。Swift 可以据此推断出 Element 的类型即是 Item 的类型。
- 在扩展添加协议一致性 中描述了如何利用扩展让一个已存在的类型遵循一个协议,这包括使用了关联类型协议。
extension Array: Container {}
Array 的 append( _ : ) 方法和下标确保了 Swift 可以推断出 Item 具体类型。定义了这个扩展后,你可以将任意 Array 当作 Container 来使用。
protocol Container {
associatedtype Item: Equatable
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
要遵守 Container 协议,Item 类型也必须遵守 Equatable 协议。
protocol SuffixableContainer: Container {
associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
func suffix(_ size: Int) -> Suffix
}
- 在这个协议里,Suffix 是一个关联类型,就像上边例子中 Container 的 Item 类型一样。
- Suffix 拥有两个约束:
它必须遵循 SuffixableContainer 协议(就是当前定义的协议)
以及它的 Item 类型必须是和容器里的 Item 类型相同。- Item 的约束是一个 where 分句,它在下面具有泛型 Where 子句的扩展中有讨论。
extension Stack: SuffixableContainer {
func suffix(_ size: Int) -> Stack {
var result = Stack()
for index in (count-size)..<count {
result.append(self[index])
}
return result
} // 推断 suffix 结果是Stack。
}
var stackOfInts = Stack<Int>()
stackOfInts.append(10)
stackOfInts.append(20)
stackOfInts.append(30)
let suffix = stackOfInts.suffix(2) // suffix 包含 20 和 30
在上面的例子中,Suffix 是 Stack 的关联类型,也是 Stack ,所以 Stack 的后缀运算返回另一个 Stack 。
另外,遵循 SuffixableContainer 的类型可以拥有一个与它自己不同的 Suffix 类型——也就是说后缀运算可以返回不同的类型。
extension IntStack: SuffixableContainer {
func suffix(_ size: Int) -> Stack<Int> {
var result = Stack<Int>()
for index in (count-size)..<count {
result.append(self[index])
}
return result
}
// 推断 suffix 结果是 Stack。
}
func allItemsMatch<C1: Container, C2: Container>
(_ someContainer: C1, _ anotherContainer: C2) -> Bool
where C1.Item == C2.Item, C1.Item: Equatable {
// 检查两个容器含有相同数量的元素
if someContainer.count != anotherContainer.count {
return false
}
// 检查每一对元素是否相等
for i in 0..<someContainer.count {
if someContainer[i] != anotherContainer[i] {
return false
}
}
// 所有元素都匹配,返回 true
return true
}
这个函数接受 someContainer 和 anotherContainer 两个参数。
参数 someContainer 的类型为 C1,参数 anotherContainer 的类型为 C2。
C1 和 C2 是容器的两个占位类型参数,函数被调用时才能确定它们的具体类型。
前两个要求定义在函数的类型形式参数列表里,后两个要求定义在了函数的泛型 where 分句中。
第三个和第四个要求结合起来意味着 anotherContainer 中的元素也可以通过 != 操作符来比较,因为它们和 someContainer 中的元素类型相同。
这些要求让 allItemsMatch(_ : _ : ) 函数能够比较两个容器,即使它们的容器类型不同。
allItemsMatch( _ : _ : ) 函数首先检查两个容器元素个数是否相同,如果元素个数不同,那么一定不匹配,函数就会返回 false。
进行这项检查之后,通过 for-in 循环和半闭区间操作符(…<)来迭代每个元素,检查 someContainer 中的元素是否不等于 anotherContainer 中的对应元素。如果两个元素不相等,那么两个容器不匹配,函数返回 false。
如果循环体结束后未发现任何不匹配的情况,表明两个容器匹配,函数返回 true。
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.")
}
// 打印“All items match.”
上面的例子创建 Stack 实例来存储 String 值,然后将三个字符串压栈。
这个例子还通过数组字面量创建了一个 Array 实例,数组中包含同栈中一样的三个字符串。
即使栈和数组是不同的类型,但它们都遵从 Container 协议,而且它们都包含相同类型的值。
因此你可以用这两个容器作为参数来调用 allItemsMatch( _ : _ : ) 函数。
在上面的例子中,allItemsMatch( _ : _ : ) 函数正确地显示了这两个容器中的所有元素都是相互匹配的。
extension Stack where Element: Equatable {
func isTop(_ item: Element) -> Bool {
guard let topItem = items.last else {
return false
}
return topItem == item
}
}
这个新的 isTop( _ : ) 方法首先检查这个栈是不是空的,然后比较给定的元素与栈顶部的元素。
如果你尝试不用泛型 where 子句,会有一个问题:在 isTop( _ : ) 里面使用了 == 运算符,但是 Stack 的定义没有要求它的元素是符合 Equatable 协议的,所以使用 == 运算符导致编译时错误。
使用泛型 where 子句可以为扩展添加新的条件,因此只有当栈中的元素符合 Equatable 协议时,扩展才会添加 isTop(_? 方法。
if stackOfStrings.isTop("tres") {
print("Top element is tres.")
} else {
print("Top element is something else.")
}
// 打印“Top element is tres.”
struct NotEquatable { }
var notEquatableStack = Stack<NotEquatable>()
let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)
notEquatableStack.isTop(notEquatableValue) // 报错
extension Container where Item: Equatable {
func startsWith(_ item: Item) -> Bool {
return count >= 1 && self[0] == item
}
}
这个 startsWith( _ : ) 方法首先确保容器至少有一个元素,然后检查容器中的第一个元素是否与给定的元素相等。
任何符合 Container 协议的类型都可以使用这个新的 startsWith( _ : ) 方法,包括上面使用的栈和数组,只要容器的元素是符合 Equatable 协议的。
if [9, 9, 9].startsWith(42) {
print("Starts with 42.")
} else {
print("Starts with something else.")
}
// 打印“Starts with something else.”
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())
// 打印“648.9”
示例将一个 average() 方法添加到 Item 类型为 Double 的容器中。
此方法遍历容器中的元素将其累加,并除以容器的数量计算平均值。
它将数量从 Int 转换为 Double 确保能够进行浮点除法。
就像可以在其他地方写泛型 where 子句一样,你可以在一个泛型 where 子句中包含多个条件作为扩展的一部分。
用逗号分隔列表中的每个条件。
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() 则提供了容器的迭代器的访问接口。
protocol ComparableContainer: Container where Item: Comparable { }
例如:
extension Container {
subscript<Indices: Sequence>(indices: Indices) -> [Item]
where Indices.Iterator.Element == Int {
var result = [Item]()
for index in indices {
result.append(self[index])
}
return result
}
}
这个 Container 协议的扩展添加了一个下标方法,接收一个索引的集合,返回每一个索引所在的值的数组。这个泛型下标的约束如下:
- 在尖括号中的泛型参数 Indices,必须是符合标准库中的 Sequence 协议的类型。
- 下标使用的单一的参数,indices,必须是 Indices 的实例。
- 泛型 where 子句要求 Sequence(Indices)的迭代器,其所有的元素都是 Int 类型。这样就能确保在序列(Sequence)中的索引和容器(Container)里面的索引类型是一致的。
综合一下,这些约束意味着,传入到 indices 下标,是一个整型的序列。