泛型

泛型

编写适用于多种类型的代码,并指定对这些类型的要求。

泛型代码让你能根据自定义的需求,编写出适用于任意类型的、灵活可复用的函数及类型。你可避免编写重复的代码,而是用一种清晰抽象的方式来表达代码的意图。

泛型是 Swift 最强大的特性之一,很多 Swift 标准库是基于泛型代码构建的。实际上,即使你没有意识到,在 语言指南 中也是一直使用泛型。例如,Swift 的 ArrayDictionary 都是泛型集合。你可以创建一个 Int 类型数组,也可创建一个 String 类型数组,甚至可以是任意其他 Swift 类型的数组。同样,你也可以创建一个存储任意指定类型的字典,并对该类型没有限制。

泛型解决的问题

下面是一个标准的非泛型函数 swapTwoInts(::),它用于交换两个 Int 类型的值:

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

这个函数使用输入输出参数(inout)来交换 ab 的值,具体请参考 .

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(::) 函数体是一样的,唯一的区别是它们接受的参数类型IntStringDouble

在实际应用中,通常需要一个更实用更灵活的函数来交换两个任意类型的值,幸运的是,泛型代码帮你解决了这种问题。(这些函数的泛型版本已经在下面定义好了。)

泛型函数

泛型函数可适用于任意类型,下面是函数 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),而不是一个实际的类型名称(例如IntStringDouble)。占位符类型名称并不关心 T 必须是什么类型,但它要求 ab 必须是相同类型的 T,无论 T 代表什么。每次调用 swapTwoValues(::) 函数时,都会确定T的实际类型。`

泛型函数和非泛型函数的另外一个不同之处在于这个泛型函数名swapTwoValues(::)后面跟着占位类型名(T),并用尖括号括起来()。这个尖括号告诉 Swift 那个 TswapTwoValues(::) 函数定义内的一个占位类型名,因此 Swift 不会去查找名为 T 的实际类型。

swapTwoValues(::) 函数现在可以像 swapTwoInts(::) 那样调用,不同的是它能接受两个任意类型的值,条件是这两个值有着相同的类型。swapTwoValues(::) 函数被调用时,T 所代表的类型都会由传入的值的类型推断出来。

在下面的两个例子中,T 分别代表 IntString

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(::) 例子中,占位类型 T 是一个类型参数的例子,类型参数指定并命名一个占位类型,并且紧随在函数名后面,使用一对尖括号括起来(例如 )。

一旦一个类型参数被指定,你可以用它来定义一个函数的参数类型(例如 swapTwoValues(::) 函数中的参数 ab),或者作为函数的返回类型,还可以用作函数主体中的类型注解。在这些情况下,类型参数会在函数调用时被实际类型所替换。(在上面的 swapTwoValues(::) 例子中,当函数第一次被调用时,TInt 替换,第二次调用时,被 String 替换。)

你可以通过在尖括号内写多个类型参数名称,并用逗号分隔,来提供多个类型参数。

命名类型参数

大多情况下,类型参数具有描述性的名称,例如字典 Dictionary 中的 KeyValue 及数组 Array 中的 Element,这能告诉阅读代码的人这些类型参数与泛型类型或函数之间的关系。然而,当它们之间没有有意义的关系时,通常使用单个字符来表示,例如 TUV,例如上面演示函数 swapTwoValues(::) 中的 T

请使用大写字母开头的驼峰命名法来为类型参数命名,例如 TMyTypeParameter,以表明它们是占位类型,而不是一个值。

注意: 如果你不需要为类型参数命名,并且其泛型约束很简单, 那么你可以使用另一种轻量级语法替代, 正如 中所述。

泛型类型

除了泛型函数,Swift 还允许自定义泛型类型。这些自定义类、结构体和枚举可以适用于任意类型,类似于 ArrayDictionary

本节将向你展示如何编写一个名为 Stack(栈)的泛型集合类型。栈是值的有序集合,和数组类似,但比数组有更严格的操作限制。数组允许在其中任意位置插入或是删除元素。而栈只允许在集合的末端添加新的元素(称之为入栈)。类似的,栈也只能从末端移除元素(称之为出栈)。

下图展示了入栈(push)和出栈(pop)的行为:

下面展示如何编写一个非泛型版本的栈,以 Int 型的栈为例:

struct IntStack {
    var items: [Int] = []
    mutating func push(_ item: Int) {
        items.append(item)
    }
    mutating func pop() -> Int {
        return items.removeLast()
    }
}

这个结构体在栈中使用一个名为 itemsArray属性来存储值。栈提供了两个方法:push(_:)pop(),用来向栈中压入值以及从 Stack 中移除值。这些方法被标记为 mutating,因为它们需要修改结构体的 items 数组。

上面的 IntStack 结构体只能用于 Int 类型。可以定义一个泛型 Stack 结构体,从而能够处理任意类型的值。

下面是 Stack 的泛型版本:

struct Stack<Element> {
    var items: [Element] = []
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
}

Element 为待提供的类型定义了一个占位名。这种待提供的类型可以在结构体的定义中通过 Element 来引用。在这个例子中,Element 在如下三个地方被用作占位符:

由于 Stack 是泛型类型,因此可以用来创建适用于 Swift 中任意有效类型的栈,就像 ArrayDictionary 那样。

你可以通过在尖括号中写出栈中需要存储的数据类型来创建并初始化一个 Stack 实例。例如,要创建一个 String 类型的栈,可以写成 Stack()

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 个字符串

下图展示了栈顶部的 "cuatro" 出栈的过程:

泛型扩展

当对泛型类型进行扩展时,你并不需要提供类型参数列表作为定义的一部分。 相反,可以在扩展中直接使用原始类型定义中的类型参数列表,并且这些来自原始类型中的参数名称会被用作原始定义中类型参数的引用。

下面的例子扩展了泛型类型 Stack,为其添加了一个名为 topItem 的只读计算属性,它将会返回当前栈顶元素且不会将其从栈中移除:

extension Stack {
    var topItem: Element? {
        return items.isEmpty ? nil : items[items.count - 1]
    }
}

topItem 属性会返回 Element 类型的可选值。

计算属性 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 的 Dictionary 类型对字典的键的类型做了些限制。在 中,字典键的类型必须是可哈希的(hashable)。也就是说,必须有一种方法能够唯一地表示它。字典键之所以要是可哈希的,是为了便于检查字典中是否已经包含某个特定键的值。若没有这个要求,字典将无法判断是否可以插入或替换某个指定键的值,也不能查找到已经存储在字典中的指定键的值。

这个要求通过对字典键类型的类型约束来强制执行,该约束指定键类型必须遵循Swift标准库中定义的 Hashable 协议。Swift的所有基本类型(如 StringIntDoubleBool)默认都是可哈希的。 如何让自定义类型遵循 Hashable 协议,可以查看文档 遵循 Hashable 协议.

你可以在创建自定义泛型类型时定义自己的类型约束,这些约束为泛型编程提供了强大的功能。像 Hashable 这样的抽象概念根据类型的概念特征而不是其具体类型来描述类型。

类型约束语法

你可以通过在类型参数的名称后添加一个类或协议约束,并用冒号分隔,来编写类型约束。下面将展示泛型函数中类型约束的基本语法(与泛型类型的语法相同):

func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
    // function body goes here
}

上面这个函数有两个类型参数。第一个类型参数 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)")
}
// Prints "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 并不会让我们对所有这类问题无从下手。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 的类型,如 DoubleString

let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])
// doubleIndex is an optional Int with no value, because 9.3 isn't in the array
let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"])
// stringIndex is an optional Int containing a value of 2

关联类型

定义一个协议时,声明一个或多个关联类型作为协议定义的一部分将会非常有用。关联类型为协议中的某个类型提供了一个占位符名称,其代表的实际类型在协议被遵循时才会被指定。关联类型通过 associatedtype 关键字来指定。

关联类型实践

下面例子定义了一个 Container 协议,该协议定义了一个关联类型 Item

protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

Container 协议定义了三个任何遵循该协议的类型(即容器)必须提供的功能:

该协议没有指定容器中的元素的类型以及如何存储。该协议只指定了任何遵循 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 协议的要求时,指定 ItemInt 类型,即 typealias Item = Int,从而将 Container 协议中抽象的 Item 类型转换为具体的 Int 类型。

得益于 Swift 的类型推断机制, 实际上在 IntStack 的定义中不需要声明 ItemInt。因为 IntStack 遵循 Container 协议的所有要求,Swift 只需通过 append(_:) 方法的 item 参数类型和下标返回值的类型,就可以推断出 Item 的具体类型。事实上,如果你在上面的代码中删除了 typealias Item = Int 这一行,一切也可正常工作,因为 Swift 清楚地知道 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 协议, 其要求关联类型 Item 必须遵循 Equatable 协议:

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 是一个关联类型,就像上边例子中 ContainerItem 类型一样。Suffix 拥有两个约束:它必须遵循 SuffixableContainer 协议(就是当前定义的协议),以及它的 Item 类型必须是和容器里的 Item 类型相同。Item 的约束是一个 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

在上面的例子中,SuffixStack 的关联类型,也是 Stack ,所以 Stack 的后缀运算返回另一个 Stack 。另外,遵循 SuffixableContainer 的类型可以拥有一个与它自己不同的 Suffix 类型——也就是说后缀运算可以返回不同的类型。比如说,这里有一个非泛型 IntStack 类型的扩展,它遵循了 SuffixableContainer 协议,使用 Stack 作为它的后缀类型而不是 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 子句后跟一个或者多个针对关联类型的约束,以及一个或多个类型参数和关联类型间的相等关系。你可以在函数体或者类型的大括号之前添加 where 子句。

下面的例子定义了一个名为 allItemsMatch 的泛型函数,用来检查两个 Container 实例是否包含相同顺序的相同元素。如果所有的元素能够匹配,那么返回 true,否则返回 false

被检查的两个 Container 可以不是相同类型的容器(虽然它们可以相同),但它们必须拥有相同类型的元素。这个要求通过一个类型约束以及一个 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
}

这个函数接受 someContaineranotherContainer 两个参数。参数 someContainer 的类型为 C1,参数 anotherContainer 的类型为 C2C1C2 是容器的两个占位类型参数,函数被调用时才能确定它们的具体类型。

这个函数的类型参数列表还定义了对两个类型参数的要求:

前两个要求定义在函数的类型形式参数列表里,后两个要求定义在函数的泛型 where 分句中。

这些要求意味着:

第三个和第四个要求结合起来意味着 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.")
}
// 打印 "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 的定义并不要求其元素遵循 Equatable 协议的,因此使用 == 操作符会导致编译时错误。使用泛型 where 子句可以为扩展添加新的条件,这样扩展只在栈中的元素遵循 Equatable 协议时才添加 isTop(_:) 方法。

以下是 isTop(_:) 方法的实际运行方式:

if stackOfStrings.isTop("tres") {
    print("Top element is tres.")
} else {
    print("Top element is something else.")
}
// 打印 "Top element is tres."

如果尝试在其元素不遵循 Equatable 协议的栈上调用 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 协议的类型,包括上面使用的栈和数组,只要容器中的元素是遵循 Equatable 的。

if [9, 9, 9].startsWith(42) {
    print("Starts with 42.")
} else {
    print("Starts with something else.")
}
// Prints "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 子句。例如,你可以在泛型类型的下标或泛型类型扩展中的方法上编写泛型 where 子句。Container 结构是泛型的,下面示例通过 where 子句让新的方法声明其调用所需要满足的类型约束。

extension Container {
    func average() -> Double where Item == Int {
        var sum = 0.0
        for index in 0..<count {
            sum += Double(self[index])
        }
        return sum / Double(count)
    }
    func endsWith(_ item: Item) -> Bool where Item: Equatable {
        return count >= 1 && self[count-1] == item
    }
}
let numbers = [1260, 1200, 98, 37]
print(numbers.average())
// 打印 "648.75"
print(numbers.endsWith(37))
// 打印 "true"

这个示例在 Container 中添加了一个当元素是整数时可使用的 average() 方法;还添加了一个当元素遵循 equatable 协议时使用 endsWith(_:) 方法。这两个函数都包含一个泛型 where 子句,该子句为 Container 原始声明中的泛型 Item 类型参数添加了类型约束。

如果你想在不使用上下文 where 子句的情况下编写这段代码,你需要为每个泛型 where 子句编写两个扩展。上面的示例和下面的示例具有相同的行为。

extension Container where Item == Int {
    func average() -> Double {
        var sum = 0.0
        for index in 0..<count {
            sum += Double(self[index])
        }
        return sum / Double(count)
    }
}
extension Container where Item: Equatable {
    func endsWith(_ item: Item) -> Bool {
        return count >= 1 && self[count-1] == item
    }
}

在使用上下文 where 子句的示例中,由于每个方法的泛型 where 子句都声明了需要满足的要求,因此average()endsWith(_:) 的实现都可以放在在同一个扩展中。将这些要求移动到扩展的泛型 where 子句进行声明也能起到同样的效果,但每一个扩展只能有一个必备条件。

具有泛型 Where 子句的关联类型

你可以在关联类型上后面加上一个泛型 where 子句。例如,假设你想创建一个包含迭代器(Iterator)的 Container,类似于 Swift 标准库中的 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 参数的值是一个整型序列。

← 返回目录