案例代码下载
泛型
泛型代码能够根据定义的要求编写可以使用任何类型的灵活,可重用的函数和类型。可以编写避免重复的代码,并以清晰,抽象的方式表达其意图。
泛型是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
}
该函数利用输入输出参数交换的值a和b,如In-Out参数所描述。
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)")
该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(::)函数体相同。但是,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的实际类型。
现在可以以swapTwoInts相同的方式调用swapTwoValues(::)函数,除了它可以传递任何类型的两个值,只要这两个值都是彼此相同的类型。每次调用swapTwoValues(::)时,要使用的类型T都是从传递给函数的值类型推断出来的。
在下面的两个例子中,T分别被推断为Int和String:
var someInt = 3
var anotherInt = 97
swapTwoValues(&someInt, &anotherInt)
var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
注意: 上面定义的swapTwoValues(::)函数的灵感来自一个叫做swapSwift标准库的泛型函数,它可以自动供在应用程序中使用。如果需要在自己的代码中使用swapTwoValues(::)函数的行为,则可以使用Swift现有的swap(::)函数,而不是提供自己的实现。
类型参数
在上面的swapTwoValues(::)示例中,占位符T类型是类型参数的示例。类型参数指定并命名占位符类型,并在函数名称后面立即写入一对匹配的尖括号(例如<T>)之间。
一旦指定一个类型参数,可以用它来定义一个函数的参数(如在swapTwoValues(::)函数的类型参数a,b),或作为函数的返回类型,或者作为函数体中的一个类型的注释。在每种情况下,只要调用函数,就会用实际类型替换类型参数。在上面的(swapTwoValues(::)例子中,第一次调用函数T被Int替换,第二次被String替换)。
可以通过在尖括号内写入多个类型参数名称来提供多个类型参数,以逗号分隔。
命名类型参数
在大多数情况下,类型参数具有描述性名称,例如Dictionary<Key, Value>中的Key和Valuein,ElementArray<Element>中的Element ,它告诉读者类型参数与其使用的泛型类型或函数之间的关系。但是,当它们之间没有有意义的关系时,这是传统的给它们命名使用单个字母,例如T,U和V,如在上述swapTwoValues(::)函数的T。
注意: 类型参数始终按上面的驼峰名称法(例如T和MyTypeParameter),以表明它们是类型的占位符,而不是值。
泛型类型
除了泛型函数,Swift还允许定义自己的泛型类型。这些是可以使用任何类型的自定义类,结构和枚举,方式与Array和Dictionary类似。
本节介绍如何编写名为Stack的泛型集合类型。栈是一组有序的值,类似于数组,但具有比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()
}
}
此结构使用一个Array属性名为items的将值存储在栈中。Stack提供了两种方法,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()
}
请注意泛型版本与非泛型版本的栈基本相同,但是调用的是类型参数Element而不是实际类型Int。此类型参数写在结构名称后面的一对尖括号(<Element>)中。
Element为稍后提供的类型定义占位符名称。这种称为Element的未来类型可以在结构定义中的任何位置使用。在这种情况下,Element在三个地方用作占位符:
- 创建一个名为items的属性,该属性使用Element类型的空数组值初始化
- 指定push(_:)方法具有一个名为item的参数,该参数必须是Element类型
- 指定pop()方法返回Element类型的值
因为它是一个泛型,Stack可用任何Swift有效的类型来创建,跟Array和Dictionary类似。
Stack通过在尖括号内写入要存储在栈中的类型来创建新实例。例如,要创建新的字符串栈,编写Stack<String>():
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
以下是stackOfStrings将这四个值推送到栈后的方式:
从栈中弹出一个值会删除并返回栈顶值,"cuatro":
let fromTheTop = stackOfStrings.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).")
}
泛型类型的扩展还可以包括扩展类型的实例必须满足的要求才能获得新功能,如下面的泛型where字句的扩展所述。
类型约束
swapTwoValues(::)函数和Stack类型可以与任何类型工作。然而对泛型函数和泛型类型使用的类型强制执行某些类型约束有时很有用。类型约束指定类型参数必须从特定类继承,或遵守特定协议或协议组合。
例如,Swift的Dictionary类型限制了可用作字典键的类型。如字典中所述,字典键的类型必须是哈希化的。也就是说,它必须提供一种可唯一表示的方法。Dictionary需要其键可以哈希化,以便它可以检查它是否已包含特定键的值。如果没有此要求,则Dictionary无法判断是否应插入或替换特定键的值,也无法为字典中已有的给定键找到值。
此要求由键类型的类型约束强制执行,Dictionary类型约束指定键类型必须符合Hashable协议,即Swift标准库中定义的特殊协议。所有wift的基本类型(例如String,Int,Double,和Bool)默认情况下可哈希。
可以在创建自定义泛型类型时定义自己的类型约束,这些约束提供了泛型编程的大部分功能。抽象概念就像Hashable根据概念特征而不是具体类型来描述类型。
类型约束语法
可以通过在类型参数的名称后面放置单个类或协议约束来写入类型约束,并以冒号分隔,作为类型参数列表的一部分。下面显示了泛型函数的类型约束的基本语法(尽管泛型类型的语法相同):
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// 这里是函数主体
}
上面的假设函数有两个类型参数。第一个类型参数T有一个类型约束,它需要T是一个SomeClass子类。第二个类型参数U具有需要U遵守SomeProtocol协议的类型约束。
类型约束实践
这是一个非泛型函数findIndex(ofString:in:),它被赋予一个String值和一个String值的数组来从中找到它。findIndex(ofString:in:)函数返回一个可选Int值,该值将是数组中第一个匹配字符串的索引(如果已找到),或者找不到该字符串就返回nil:
func findIndex(ofString valueToFound: String, in array: [String]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFound {
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)")
}
但是,在数组中查找值的索引的规则不仅对字符串有用。可以通过用某个类型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,表示“遵守Equatable协议的任何类型T”。
findIndex(of:in:)函数现在可以成功编译,并且可以与任何遵守Equatable协议的类型使用,例如:Double或String:
let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])
let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"])
关联类型
在定义协议时,将一个或多个关联类型声明为协议定义的一部分有时很有用。一个关联类型给出了一个占位符名称到被用作协议的一部分的类型。在采用协议之前,不会指定用于该关联类型的实际类型。使用associatedtype关键字指定关联类型。
关联类型实践
这是一个名为Container的协议示例,它声明了一个名为Item的关联类型:
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
Container协议定义了任何容器必须提供的三个必需功能:
- 必须可以使用append(_:)方法将新项添加到容器中。
- 必须可以通过返回Int值的count属性访问容器中项目的计数。
- 必须能够使用带有Int索引值的下标检索容器中的每个项目。
此协议未指定应如何存储容器中的项目或允许它们使用的类型。该协议仅指定任何类型必须提供的三个功能位才能被视为Container。遵守的类型可以提供附加功能,只要它满足这三个要求即可。
任何遵守合Container协议的类型都必须能够指定它存储的值的类型。具体来说,它必须确保只将正确类型的项添加到容器中,并且必须清楚其下标返回的项的类型。
要定义这些要求,Container协议需要一种方法在不知道特定容器的类型的情况下来引用容器将容纳的元素的类型。Container协议需要指定传递给append(_:)方法的任何值必须和容器的元素具有相同的类型,以及通过容器的下标所返回的值必须和容器的元素具有相同的类型。
为此,Container协议声明了一个名为Item的关联类型,写为associatedtype Item。该协议没有定义Item么是什 - 提供符合类型的任何信息。尽管如此,别名Item在Container中提供了一种方法来引用项目的类型,并定义与append(_:)方法和下标一起使用的类型,以确保任何Container的执行是预期的行为。
这里的从上方的泛型类型的非泛型的版本IntStack,遵守Container协议:
struct IntStack: Container {
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
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实现,Item使用为Int类型。typealias Item = Int定义将抽象类型Item转换为Container协议实现的具体类型Int。
由于Swift的类型推断,实际上并不需要声明一个具体Item的Int作为IntStack定义的一部分。因为IntStack遵守Container协议的所有要求,Swift可以简单地通过查看append(_:)方法的item参数类型和下标的返回类型推断来适当的使用Item。实际上,如果从上面的代码中删除typealias Item = Int,一切仍然有效,因为它清楚Item应该使用什么类型。
还可以使泛型Stack类型遵守Container协议:
struct Stack<Element>: Container {
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
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满足该协议。使用空扩展名执行此操作,如使用扩展名声明协议采用中所述:
extension Array: Container { }
Array的现有append(_:)方法和下标使Swift能够推断出适用的类型Item,就像上面的泛型类型Stack一样。定义此扩展后,可以使用任何Array作为Container。
将约束添加到关联类型
可以将类型约束添加到协议中的关联类型,以要求遵守的类型满足这些约束。例如,以下代码定义了一个版本Container要求容器中的项遵守equatable协议。
protocol Container {
associatedtype Item: Equatable
mutating func append(_ item: Item)
var count: Item { 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是一个关联类型,如上例中的Item类型Container。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.push(self[index])
}
return result
}
}
var stackOfInts = Stack<Int>()
stackOfInts.append(10)
stackOfInts.append(20)
stackOfInts.append(30)
let suffix = stackOfInts.suffix(2)
在上面的示例中,Stack的关联类型Suffix也是Stack,因此Stack的suffix操作返回另一个Stack。或者,遵守SuffixableContainer的类型可以具有与其Suffix自身不同的类型 - 意味着suffix操作可以返回不同的类型。例如,这是一个遵守SuffixableContainer协议的非泛型类型IntStack的扩展,使用Stack<Int>作为suffix类型代替IntStack:
extension IntStack: SuffixableContainer {
func suffix(_ size: Int) -> Stack<Int> {
var result = Stack<Int>()
for index in count - size..<count {
result.push(self[index])
}
return result
}
}
泛型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
}
}
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。
如果循环结束而没有找到不匹配,则两个容器匹配,函数返回true。
以下是该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.")
}
上面的示例创建了一个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.")
}
如果尝试在其元素不相等的栈上调用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 startWith(_ item: Item) -> Bool {
return count >= 1 && item == self[0]
}
}
startsWith(:)方法首先确保容器至少有一个项,然后检查容器中的第一个项是否与给定项匹配。只要容器的项是可比较的,这个新startsWith(:)方法可以用于符合Container协议的任何类型,包括上面使用的堆栈和数组。
if [9, 9, 9].startWith(42) {
print("Starts with 42.")
} else {
print("Starts with something else.")
}
上例中的泛型where子句要求Item符合协议,但也可以编写特定Item类型的泛型where子句。例如:
extension Container where Item == Double {
func average() -> Double {
var sum = 0.0
for index in 0..<count {
sum += self[index]
}
return sum / Double(count)
}
}
此示例向Item类型为Double的容器添加average()方法。它迭代容器中的项将它们相加,并除以容器的count以计算平均值。它显式地将count从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子句。在subscript之后的尖括号内写入占位符类型名称,并在下标主体的左大括号前写一个泛型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参数传递的值是整数序列。