泛型
泛型代码 让你根据你定义的要求,编写可以使用任何类型的灵活、可重用的函数和类型。你可以避免编写重复的代码,并且以一种清晰和抽象的方式去表达其意图。
泛型是 Swift 最强大的特性之一,而且 Swift 标准库大部分内容都是使用泛型构建的。 实际上,即使你没有意识到,你也一直在这本 Language Guide 中使用泛型。 例如,Swift 的 Array
和 Dictionary
类型都是泛型集合。你可以创建一个包含 Int
值的数组,或者有个包含 String
类型的数组,或者实际上是一个可以在 Swift 中创建的任何其他类型的数组。 同样的,你可以创建一个字典去存储任意指定类型的值,并且对该类型也没有限制。
泛型解决的问题#
这里有一个标准的非泛型函数 swapTwoInts(_:_:)
,交换了 2 个 Int
值:
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
这个函数利用 in-out 参数来交换 a
和 b
的值,具体请参考 In-Out Parameters.
swapTwoInts(_:_:)
函数将 b
的原始值换成了 a
,a
的原始值换成了 b
. 你可以调用这个函数去交换 2 个 Int
类型的变量:
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// Prints "someInt is now 107, and anotherInt is now 3"
swapTwoInts(_:_:)
是非常有用的,但是他只适用于 2 个 Int
类型的值。如果你想要交换 2 个 String
类型的值,或者 2 个 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
)
编写可以交换 任何 类型的 2 个值的单个函数更加有用,更灵活。泛型代码可以帮助你编写这样一个函数(这些函数的泛型版本已经在下面定义好了。)
注意
在这所有的 3 个函数中,
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(_:_:)
函数的函数体里面的内容完全相同。但是, 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
,无论 T
代表什么。每次调用 swapTwoValues(_:_:)
的时候,才会确定代替 T
的实际类型。
泛型函数和非泛型函数的另一个区别是泛型函数的名称, swapTwoValues(_:_:)
后面的是占位类型名( T
)被包含在尖括号里面( <T>
)。括号告诉 Swift , T
是 swapTwoValues(_:_:)
函数定义的占位类型名。因为 T
是占位符,所以 Swift 不会去查找名为 T
的实际类型。
现在 swapTwoValues(_:_:)
函数可以像 swapTwoInts
一样被调用,不同的是它可以接收 2 个任意类型的值,只要这些值是相同类型。每次 swapTwoValues(_:_:)
被调用的时候,从传递给函数的值的类型就可以推断出 T
所代表的类型。
下面的 2 个例子中,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
,第二次调用时 T
被替换成 String
。)
你可以通过在尖括号内写多个类型参数名来提供多个类型参数,用逗号隔开。
命名类型参数#
大多数情况下,类型参数会有一个描述性的名称,例如在 Dictionary<Key, Value>
中的 Key
和 Value
,以及 Array<Element>
中的 Element
,这能告诉读者关于类型参数与泛型类型或函数之间的关系。然而,当它们之间没有一种有意义的关系时,通常使用单个字母进行命名,例如 T
,U
,和 V
,例如上面提到的函数 swapTwoValues(_:_:)
中的 T
。
注意
请始终使用首字母大写的驼峰命名方式(例如
T
和MyTypeParameter
)去表明它们是一个类型占位,不是一个值。
泛型类型#
除了泛型函数外,Swift 可以定义你自己的 泛型类型 。这些自定义的类、结构体、枚举可以和 任何 类型一起使用,方式类似于 数组
和 字典
。
本节向你展示如何写一个叫做 栈
的泛型集合类型。栈是一种有序数值集合,与数组类似,但是其操作比 数组
更严格。对数组而言新元素可以在任意位置插入移除。但是对栈而言,新元素只允许追加到集合的末尾(入栈)。同理,移除元素也只允许在末尾处移除(出栈)。
注意
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()
}
}
这个结构体使用一个叫做 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
的泛型版本和非泛型版本是一样的,它们的区别仅仅只是使用 Element
类型的参数代替 Int
类型的参数。泛型版本参数的写法是在结构体名后面跟上一对尖括号 (<Element>
)。
Element
为后续类型提供了一个占位符。在结构体定义范围内的任意位置都可以使用 Element
这种类型。本例中 Element
有三个地方是作为占位符使用的:
- 创建一个叫做
items
的属性,用Element
类型的空数组进行初始化。 - 指定
push(_:)
方法中的item
参数,必须是Element
类型。 - 指定
pop()
方法的返回值是Element
类型。
因为这是一个泛型类型,和 Array
、Dictionary
用法类似, Stack
可以使用 任意 有效的 Swift 类型来创建一个栈。
你可以通过向尖括号内写入要存储在栈中的类型来创建新的 Stack
实例。 例如,要创建一个新的字符串栈,可以编写 Stack<String>()
:
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// 栈现在包含4个字符串
以下是在将这四个值推入 stackOfStrings
栈之后的样子:
从栈中 pop 一个值会删除并返回栈顶值,「cuatro」
:
let fromTheTop = stackOfStrings.pop()
// fromTheTop 等于「cuatro」,堆栈现在包含3个字符串
这是 pop 栈顶值后的栈:
扩展泛型类型#
扩展泛型类型时,你不需要提供类型参数列表作为扩展定义的一部分。 相反,原始 类型定义中的类型参数列表在扩展的主体内依旧可用,并且原始类型参数名称会被用于引用原始定义中的类型参数。
下面的示例扩展了一个泛型 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."
为了扩展新的功能,泛型类型的扩展还可以要求扩展类型的实例必须满足特定的约束,如 扩展泛型约束 中所述。
类型约束#
上例中 swapTwoValues(_:_:)
函数和 Stack
类型可以使用任意类型。但是有时候对泛型函数和泛型类型进行 类型约束 是很有用的。 类型约束指定参数类型必须继承自特定的类、遵循特定的协议、特定的协议组。
例如,Swift 中 字典
类型对可以用作键的类型做了类型限制。在 字典中有描述 ,字典的键的类型必须是 可哈希化的 。也就是说它必须提供一种方式让自己具有唯一性。字典
的键必须是能够进行哈希的以确保对应的键有对应的值。如果没有这种要求,字典就无法辨别对于指定的键是记性插入还是替换值的操作,也无法根据给定的键去查找对应的值。
这个要求是 字典
的键类型的类型约束强制执行的,键的类型必须遵循 能够哈希化
协议,Swift 标准库中定义的一种特殊协议。所有 Swift 的基本类型(例如: String
, Int
, Double
,和 Bool
)默认都能够进行哈希化。
当创建自定泛型时,你可以定义你自己的类型约束,这些约束让泛型程序更强大。像 可哈希化
这种抽象概念依据他们的概念特征来描述类型,而不是他们的具体类型。
类型约束语法#
类型约束的写法:在类型参数名后跟上一个类或协议来进行约束,使用冒号进行分割,作为类型参数列表的一部分。泛型函数的类型约束语法如下(泛型类型的语法与此相同):
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// 这里写函数体的内容
}
假设上述函数有两种类型的参数。第一个类型参数 T
,类型约束要求 T
的类型必须是 SomeClass
的子类。第二个类型参数 U
,类型约束要求 U
必须遵循 SomeProtocol
协议。
类型约束行为#
这是一个叫做 findIndex(ofString:in:)
的非泛型函数,给定一个 字符串
查找其在字符串数组中的位置。 findIndex(ofString:in:)
函数的返回值是一个 Int
型的可选类型,如果在数组中找到第一个匹配的字符串就返回对应的下标,找不到就返回 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
类型。
你可能会这样写 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
, 意味着 「任何类型 T
都遵循 Equatable
协议 。」
现在 findIndex(of:in:)
函数编译成功了,因为遵循了 Equatable
协议,现在参数可以使用任何类型,例如:Double
或者 String
:
let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])
// doubleIndex 是一个没有值的 Int 可选类型,因为数组中没有9.3这个值
let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"])
// stringIndex 是一个值为 2 的 Int 可选类型
关联类型#
当定义一个协议时,有时候定义一个或多个关联类型作为协议的一部分是很有用的。关联类型 作为协议的一部分并为一种类型提供占位符名称。在实现该协议之前不会指定该关联类型的实际类型。关联类型使用 associatedtype
关键字来指定。
关联类型实战#
这是一个 Container
协议的示例,该协议有一个关联类型叫做 Item
:
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
Container
协议定义了任何容器必须提供以下三个功能:
- 必须可以使用
append(_:)
方法将新元素添加到容器中。 - 必须可以通过
count
属性来获得容器中元素的数量,并且返回Int
值。 - 必须可以使用
Int
索引值的下标来检索容器中的每个元素。
该协议并没有指定容器中元素的类型以及如何存储它们。仅仅指定了能被视为 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
协议的所有三个要求,并且在每种情况下都包含部分 IntStack
原有的功能以满足这些要求。
此外,IntStack
指定了对于 Container
的这个实现,使用 Int
作为 Item
的对应类型。typealias Item = Int
这行定义将 Item
的抽象类型转换为具体类型的 Int
。
得益于 Swift 的类型推断,实际上在 IntStack
的定义中不需要声明 Item
为 Int
。因为 IntStack
符合 Container
协议的所有要求,所以 Swift 可以通过查看 append(_:)
方法中 item
参数的类型和下标的返回类型来推断出相应的 Item
类型。实际上,如果从上面的代码中删除 typealias Item = Int
这行,一切仍然有效,因为很清楚 Item
应该使用什么类型。
你也同样可以创建泛型 Stack
类型来符合 Container
协议:
struct Stack<Element>: Container {
// Stack<Element> 的原始实现
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
的合适类型。
扩展现有类型以指定关联类型#
你可以通过扩展现有类型来保证协议的一致性,如 添加协议与扩展的一致性 中所述。这需要一个带有关联类型的协议。
Swift 的 Array
类型已经提供了 append(_ :)
方法,count
属性,以及带有 Int
索引的下标来检索它的元素。这三个功能符合 Container
协议的要求。这意味着只需声明 Array
遵循了 Container
协议,就可以扩展 Array
使其符合 Container
协议。你可以使用空扩展执行此操作,如 使用扩展声明遵循协议 中所述:
extension Array: Container {}
根据 Array 现有的 append(_:)
方法和下标,Swift 能够推断出 Item
的具体类型,就像上面的泛型 Stack
类型一样。定义此扩展后,你可以把任何 Array
当作 Container
使用。
将约束添加到关联类型#
你可以将类型约束添加到协议的关联类型中,以要求符合的类型满足这些约束。例如,以下代码定义了一个 Container
版本,它要求容器中的项目是相等的。
protocol Container {
associatedtype Item: Equatable
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
为了遵循这个版本的 Container
,容器的 Item
类型必须符合 Equatable
协议。
在其关联类型的约束中使用协议#
协议可以作为其自身要求的一部分出现。例如,有一个 Container
协议的改进版,添加了 suffix(_:)
方法。 suffix(_:)
方法从容器的末尾返回给定数量的元素,并将它们存储在 Suffix
类型的实例中。
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 子句的关联类型 中提到。
这是上面 强引用循环闭包 中 Stack
类型的扩展,它实现了 SuffixableContainer
协议:
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
在上面的例子中,Stack
的关联类型 Suffix
也是 Stack
,因此 Stack
中的 suffix 方法返回另一个 Stack
。不过,符合 SuffixableContainer
的类型也可以具有与其自身不同的 Suffix
类型 --- 这意味着 suffix 方法可以返回不同的类型。 例如,下面是非泛型 IntStack
类型的扩展,它实现了 SuffixableContainer
协议,使用 Stack <Int>
作为其 suffix 方法的返回类型而不是 IntStack
:
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<Int> 类型.
}
泛型 Where 子句#
正如 类型约束 中所述,类型约束允许你对泛型函数的类型参数,下标或者类型定义一些规则。
对关联类型定义一些规定通常也很有用。你可以通过定义 泛型 where 子句 来完成此操作。泛型 where
子句使你能够要求关联类型必须符合某个协议,或者某些类型参数和相关类型必须相同。泛型 where
子句以 where
关键字开头,后跟关联类型的约束条件或类型和关联类型之间的相等关系。你需要在一个类型或函数体的起始大括号之前写一个泛型 where
子句。
下面的示例定义了一个名为 allItemsMatch
的泛型函数,它检查两个 Container
实例的顺序和对应元素是否相同。如果所有元素都匹配,则该函数返回布尔值 true
,如果不匹配,则返回值 false
。
两个被检查的容器不必是相同类型的容器(尽管它们可以是),但它们必须保证各自的元素类型都相同。这个规则通过类型约束和泛型 where
子句的组合表示:
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
都是调用函数时需要确定的两种容器类型的类型参数。
以下是对函数的两个类型参数的要求:
C1
必须符合Container
协议(写成C1:Container
)。C2
也必须符合Container
协议(写成C2:Container
)。C1
的Item
必须与C2
的Item
相同(写成C1.Item == C2.Item
)。C1
的Item
必须符合Equatable
协议(写成C1.Item:Equatable
)。
第一个和第二个要求在函数的类型参数列表中定义,第三个和第四个要求在函数的泛型 where
子句中定义。
这些要求意味着:
someContainer
是一个类型为C1
的容器。anotherContainer
是一个类型为C2
的容器。someContainer
和anotherContainer
包含相同类型的元素。- 可以使用不等运算符(
!=
)检查someContainer
中的项目,看它们是否彼此不同。
第三个和第四个要求结合起来意味着 anotherContainer
中的元素 也 可以用 !=
运算符进行检查,因为它们与 someContainer
中的元素全相同。
这些要求使 allItemsMatch(_:_:)
函数能够对两个容器进行比较,即使这两个容器是不同的类型。
allItemsMatch(_:_:)
函数首先检查两个容器是否包含相同数量的元素。 如果它们包含不同数量的元素,则无法匹配,该函数返回 false
。
在进行此项检查之后,该函数使用 for
- in
循环和半开运算符( .. <
)遍历 someContainer
中的所有元素。 对于每个元素,该函数检查来自 someContainer
的元素是否不等于 anotherContainer
中的相应元素。 如果两个元素不相等,则两个容器不匹配,函数返回 false
。
如果循环结束而没有找到不匹配,则两个容器匹配,函数返回 false
。
以下是 allItemsMatch(_:_:)
函数的具体实现:
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(_:_:)
函数正确地打印了两个容器中的所有项都匹配。
在扩展中使用泛型 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
}
}
这个新的 isTop(_:)
方法首先检查栈是否为空,然后将给定元素与栈的顶层元素进行比较。 如果你试图在没有泛型 where
子句的情况下这样做,会遇到一个问题:isTop(_:)
的实现使用了 ==
运算符,但 Stack
的定义并不要求它的元素是可相等的,因此使用 ==
运算符会导致编译时错误。 使用泛型 where
子句可以向扩展添加新的需求,这样只有当栈中的元素是可相等时,扩展才会添加 isTop(_:)
方法。
以下是 isTop(_:)
方法的实现:
if stackOfStrings.isTop("tres") {
print("Top element is tres.")
} else {
print("Top element is something else.")
}
// 打印 "Top element is tres."
如果你尝试在一个元素不可相等的栈上调用 isTop(_:)
方法,则会出现编译时错误。
struct NotEquatable { }
var notEquatableStack = Stack<NotEquatable>()
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
}
}
startsWith(_:)
方法首先确保容器至少有一个元素,然后检查容器中的第一个元素是否与给定元素匹配。 这个新的 startsWith(_:)
方法可以用于符合 Container
协议的任何类型,包括上面使用的栈和数组,只要容器的元素是可相等的。
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..<count {
sum += self[index]
}
return sum / Double(count)
}
}
print([1260.0, 1200.0, 98.6, 37.0].average())
// 打印 "648.9"
这个例子为 Item
类型为 Double
的容器添加了一个 average()
方法。 它遍历容器中的所有元素并将它们相加,最后除以容器的元素个数以计算平均值。 它显式地将元素个数从 Int
转换为 Double
以便能够进行浮点除法。
你可以在扩展的泛型 where
子句中包含多个需求,使用逗号来分隔列表中的每个需求,就像在其他地方编写的泛型 where
子句一样。
带有泛型 Where 子句的关联类型#
你可以在关联类型上加入泛型 where
子句。 例如,假设你想要创建一个包含迭代器的 Container
版本,就像 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
子句。 可以在 下标
之后的尖括号内写一个类型占位符,在下标主体的起始大括号之前写一个泛型 where
子句。 例如:
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
子句要求序列的迭代器能够遍历Int
类型的元素。 这可以确保序列中的索引类型与容器中的索引类型相同。
总之,这些约束意味着为 indices
参数传递的值是一个整数序列。
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。