Swift 5.x - 泛型(中文文档)

943 阅读25分钟

引言

继续学习Swift文档,从上一章节:协议,我们学习了Swift协议的内容,主要有使用protocol关键词声明协议,定义方式与类、结构体、枚举类似等内容。现在,我们学习Swift协议的相关内容。由于篇幅较长,这里分篇来记录,接下来,开始吧!

泛型

泛型代码使您能够编写灵活、可重用的函数和类型,这些函数和类型可以根据您定义的需求使用任何类型。您可以编写避免重复并以清晰、抽象的方式表达其意图的代码。

泛型是Swift最强大的特性之一,Swift标准库的大部分都是用泛型代码构建的。事实上,您在整个语言指南中一直在使用泛型,即使您没有意识到这一点。例如,Swift的数组和字典类型都是泛型集合。您可以创建一个包含Int值的数组,或者一个保存字符串值的数组,或者实际上是一个可以在Swift中创建的任何其他类型的数组。类似地,您可以创建一个字典来存储任何指定类型的值,并且对该类型可以是什么没有限制。

1 泛型解决的问题

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

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

此函数使用in-out参数交换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)")
// Prints "someInt is now 107, and anotherInt is now 3"

swapTwoInts(::)函数很有用,但它只能与Int值一起使用。如果要交换两个字符串值或两个Double值,则必须编写更多函数,如下面所示的swaptwotings(::)和swaptwodubles(::)函数:

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
}

您可能已经注意到swaptwoint(::)、swaptwotstrings(::)和swapTwoDoubles(:)函数的主体是相同的。只接受双精度值的类型。

编写一个交换任何类型的两个值的函数更有用,也更灵活。泛型代码使您能够编写这样的函数。(这些函数的通用版本定义如下。)

注意
在这三个函数中,a和b的类型必须相同。如果a和b不属于同一类型,则不可能交换它们的值。Swift是一种类型安全语言,不允许(例如)String类型的变量和Double类型的变量相互交换值。尝试这样做会导致编译时错误。

2 泛型函数

泛型函数可以用于任何类型。下面是上面的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一样调用,只是可以传递两个任何类型的值,只要这两个值的类型都相同。每次调用swapTwoValues(::)时,将从传递给函数的值类型中推断出用于T的类型。

在下面的两个示例中,T分别被推断为Int和String:

var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt is now 107, and anotherInt is now 3

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString is now "world", and anotherString is now "hello" 

注意
上面定义的swapTwoValues(::)函数的灵感来自一个名为swap的通用函数,swap是Swift标准库的一部分,它自动提供给您在应用程序中使用。如果需要在自己的代码中使用swapTwoValues(::)函数的行为,可以使用Swift现有的swap(::)函数,而不是提供自己的实现。

3 类型参数

在上面的swapTwoValues(::)示例中,占位符类型T是类型参数的一个示例。类型参数指定并命名一个占位符类型,并立即写入函数名之后,在一对匹配的尖括号之间(例如“T”)。

一旦指定了类型参数,就可以使用它来定义函数参数的类型(例如swapTwoValues(:)函数的a和b参数)、函数的返回类型或函数体中的类型注释。在每种情况下,每当调用函数时,类型参数都被替换为实际类型。(在上面的swapTwoValues(::)示例中,T在第一次调用函数时被替换为Int,在第二次调用时被替换为String。) 通过在尖括号内用逗号分隔多个类型参数名称,可以提供多个类型参数。

4 命名类型参数

在大多数情况下,类型参数都有描述性的名称,例如Dictionary<Key,Value>中的Key和Value以及Array (Element)中的Element,这些名称告诉读者类型参数与它所使用的泛型类型或函数之间的关系。但是,当它们之间没有意义的关系时,传统的做法是用T、U和V等单字母来命名它们,比如上面swapTwoValues(::)函数中的T。

注意
请始终为类型参数提供大小写大写的名称(例如T和MyTypeParameter),以指示它们是类型的占位符,而不是值。

5 泛型类型

除了泛型函数之外,Swift还允许您定义自己的泛型类型。这些是自定义类、结构体和枚举,可以与任何类型一起使用,其方式与数组和字典类似。

本节介绍如何编写名为Stack的泛型集合类型。堆栈是一组有序的值,类似于数组,但其操作集比Swift的数组类型更受限制。数组允许在数组中的任何位置插入和删除新项。但是,堆栈只允许将新项追加到集合的末尾(称为将新值推送到堆栈)。类似地,堆栈只允许从集合的末尾移除项(称为从堆栈中弹出一个值)。

注意
UINavigationController类使用堆栈的概念在其导航层次结构中为视图控制器建模。调用UINavigationController类pushViewController(:animated:)方法将视图控制器添加(或推送)到导航堆栈,并调用其popViewControllerAnimated(:)方法从导航堆栈中移除(或弹出)视图控制器。每当您需要严格的“后进先出”方法来管理集合时,堆栈是一个有用的集合模型。

下图显示了堆栈的推送和弹出行为:

1、堆栈上当前有三个值。 2、第四个值被压入堆栈的顶部。 3、堆栈现在保存四个值,最近的一个在顶部。 4、将弹出堆栈中的顶部项目。 5、弹出一个值后,堆栈再次保存三个值。

下面是如何编写堆栈的非泛型版本,在本例中,对于Int值堆栈:

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

此结构体使用名为items的数组属性将值存储在堆栈中。提供了两个堆栈上的和pop-off和push-off方法。这些方法被标记为变异,因为它们需要修改(或变异)结构的items数组。

但是,上面显示的IntStack类型只能与Int值一起使用。定义一个可以管理任何类型值的堆栈的泛型堆栈类会更有用。 以下是同一代码的通用版本:

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

请注意,堆栈的泛型版本与非泛型版本本质上是相同的,但其类型参数名为Element,而不是实际的Int类型。此类型参数写在结构名称后的一对尖括号(Element)中。 元素为稍后要提供的类型定义占位符名称。这种未来类型可以称为结构定义中任何地方的元素。在本例中,元素在三个位置用作占位符:

  • 创建名为items的属性,该属性使用Element类型的空值数组初始化
  • 指定push(:)方法有一个名为item的参数,该参数的类型必须是Element
  • 指定pop()方法返回的值将是Element类型的值

因为它是一个泛型类型,Stack可以用来在Swift中创建任何有效类型的堆栈,其方式与Array和Dictionary类似。

通过在尖括号内写入要存储在堆栈中的类型,可以创建一个新的堆栈实例。例如,要创建新的字符串堆栈,请编写stack<String>():

var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// the stack now contains 4 strings

以下是stackOfStrings在将这四个值推送到堆栈后的外观:

从堆栈中弹出一个值将删除并返回顶部值“cuatro”:

let fromTheTop = stackOfStrings.pop()
// fromTheTop is equal to "cuatro", and the stack now contains 3 strings

下面是栈顶值弹出后的外观:

6 扩展泛型类型

扩展泛型类型时,不提供类型参数列表作为扩展定义的一部分。相反,原始类型定义中的类型参数列表在扩展的主体中可用,原始类型参数名称用于引用原始定义中的类型参数。

以下示例扩展了泛型堆栈类型,以添加名为topItem的只读计算属性,该属性返回堆栈上的顶层项,而不会从堆栈中弹出它:

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

topItem属性返回类型为Element的可选值。如果堆栈为空,topItem返回nil;如果堆栈不为空,topItem返回items数组中的最后一个项。

注意,这个扩展没有定义类型参数列表。相反,在扩展中使用堆栈类型的现有类型参数名Element来指示topItem计算属性的可选类型。 topItem computed属性现在可以与任何堆栈实例一起使用,以访问和查询其顶层项,而无需移除它。

if let topItem = stackOfStrings.topItem {
    print("The top item on the stack is \(topItem).")
}
// Prints "The top item on the stack is tres."

泛型类型的扩展还可以包括扩展类型的实例必须满足的需求,以便获得新的功能,正如下面使用泛型Where子句的扩展中所讨论的那样。

7 类型约束

swapTwoValues(::)函数和堆栈类型可以用于任何类型。但是,有时对可以与泛型函数和泛型类型一起使用的类型强制执行某些类型约束是很有用的。类型约束指定类型参数必须从特定类继承,或符合特定协议或协议组合。

例如,Swift的字典类型对可以用作字典键的类型进行了限制。如字典中所述,字典的键的类型必须是散列的。也就是说,它必须提供一种使自己具有独特代表性的方法。Dictionary需要它的键是散列的,这样它就可以检查它是否已经包含一个特定键的值。如果没有这个要求,Dictionary就无法判断是否应该插入或替换特定键的值,也无法为字典中已经存在的给定键查找值。

这个要求是由Dictionary的key type上的一个type约束来实现的,它指定键类型必须符合Hashable协议,Hashable协议是Swift标准库中定义的一种特殊协议。默认情况下,Swift的所有基本类型(如String、Int、Double和Bool)都是散列的。有关使您自己的自定义类型符合哈希协议的信息,请参阅符合哈希协议

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

7.1 类型约束语法

编写类型约束的方法是将单个类或协议约束放在类型参数的名称后(用冒号分隔),作为类型参数列表的一部分。泛型函数上类型约束的基本语法如下所示(尽管泛型类型的语法相同):

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

上面的假设函数有两个类型参数。第一个类型参数T有一个类型约束,它要求T是SomeClass的子类。第二个类型参数U有一个类型约束,它要求U符合协议SomeProtocol。

7.2 类型约束实践

这是一个称为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)")
}
// Prints "The index of llama is 2"

但是,在数组中查找值的索引的原则并不是只对字符串有用。您可以编写与泛型函数相同的功能,方法是用某种类型的值替换字符串。 下面是您对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协议。

任何可等价的类型都可以安全地与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:equalable,这意味着“任何符合equalable协议的类型T。”

findIndex(of:in:)函数现在可以成功编译,并且可以用于任何可相等的类型,例如Double或String:

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

8 关联类型

在定义协议时,有时将一个或多个关联类型声明为协议定义的一部分很有用。关联类型为用作协议一部分的类型提供占位符名称。在采用协议之前,不会指定要用于该关联类型的实际类型。关联类型是用associatedtype关键字指定的。

8.1 关联类型实践

下面是一个名为Container的协议的示例,它声明了一个名为Item的关联类型:

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

容器协议定义了任何容器必须提供的三种必需功能:

  • 必须能够使用append(:)方法向容器添加新项。
  • 必须能够通过返回Int值的count属性访问容器中的项的计数。
  • 必须能够使用一个接受Int索引值的下标检索容器中的每个项。

这个协议没有指定容器中的项目应该如何存储或者允许它们是什么类型的。协议只指定任何类型必须提供的三位功能才能被视为容器。一个一致的类型可以提供额外的功能,只要它满足这三个要求。

任何符合容器协议的类型都必须能够指定它存储的值的类型。具体地说,它必须确保只有正确类型的项添加到容器中,并且必须清楚其下标返回的项的类型。

为了定义这些需求,容器协议需要一种方法来引用容器将容纳的元素的类型,而不知道特定容器的类型。容器协议需要指定传递给append(:)方法的任何值的类型必须与容器的元素类型相同,并且容器的下标返回的值将与容器的元素类型相同。

为了实现这一点,容器协议声明了一个名为Item的关联类型,并将其写成associatedtype Item。该协议没有定义什么项目是信息留给任何一致的类型提供。尽管如此,项别名提供了一种方法来引用容器中的项的类型,并定义了一个用于append(:)方法和下标的类型,以确保任何容器的预期行为都得到实施。

下面是来自上述泛型类型的非泛型IntStack类型的一个版本,它经过调整以符合容器协议:

struct IntStack: Container {
    // original IntStack implementation
    var items = [Int]()
    mutating func push(_ item: Int) {
        items.append(item)
    }
    mutating func pop() -> Int {
        return items.removeLast()
    }
    // conformance to the Container protocol
    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类型实现了容器协议的所有三个要求,并且在每种情况下都包装了IntStack类型的现有功能的一部分,以满足这些要求。

此外,IntStack指定对于容器的这个实现,要使用的适当项是Int类型。typealias Item=Int的定义将抽象的Item类型转换为具体的Int类型,以实现容器协议。

由于Swift的类型推断,实际上不需要声明Int的具体项作为IntStack定义的一部分。因为IntStack符合容器协议的所有要求,Swift可以通过查看append(:)方法的Item参数的类型和下标的返回类型来推断要使用的适当项。事实上,如果从上面的代码中删除typealias Item=Int行,那么一切仍然有效,因为很清楚应该为Item使用什么类型。

也可以使通用堆栈类型符合容器协议:

struct Stack<Element>: Container {
    // original Stack<Element> implementation
    var items = [Element]()
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
    // conformance to the Container protocol
    mutating func append(_ item: Element) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Element {
        return items[i]
    }
}

这一次,type parameter元素被用作append(:)方法的item参数的类型和下标的返回类型。因此,Swift可以推断Element是作为这个特定容器的项的适当类型。

8.2 扩展现有类型以指定关联类型

可以扩展现有类型以向协议添加一致性,如在使用扩展添加协议一致性中所述。这包括一个具有关联类型的协议。

Swift的数组类型已经提供了append(:)方法、count属性和一个带有Int索引的下标来检索其元素。这三种功能符合容器协议的要求。这意味着您可以通过声明Array采用协议来扩展Array以符合容器协议。使用空扩展执行此操作,如使用扩展声明协议采用中所述:

extension Array: Container {}
8.3 向关联类型添加约束

您可以向协议中的关联类型添加类型约束,以要求一致性类型满足这些约束。例如,下面的代码定义了一个要求容器中的项相等的容器版本。

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

要符合此版本的容器,容器的项类型必须符合Equatable协议。

8.4 在关联类型的约束中使用协议

协议可以作为其自身需求的一部分出现。例如,这里有一个改进容器协议的协议,添加了suffix(:)方法的要求。suffix(:)方法从容器的末尾返回给定数量的元素,并将它们存储在suffix类型的实例中。

protocol SuffixableContainer: Container {
    associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
    func suffix(_ size: Int) -> Suffix
}

在这个协议中,Suffix是一个关联的类型,就像上面容器示例中的Item类型一样。Suffix有两个约束:它必须符合SuffixableContainer协议(当前正在定义的协议),并且它的Item类型必须与容器的Item类型相同。Item上的约束是一个泛型where子句,在下面的泛型where子句的关联类型中对此进行了讨论。

下面是从上面的泛型类型扩展的堆栈类型,它增加了对SuffixableContainer协议的一致性:

extension Stack: SuffixableContainer {
    func suffix(_ size: Int) -> Stack {
        var result = Stack()
        for index in (count-size)..<count {
            result.append(self[index])
        }
        return result
    }
    // Inferred that Suffix is Stack.
}
var stackOfInts = Stack<Int>()
stackOfInts.append(10)
stackOfInts.append(20)
stackOfInts.append(30)
let suffix = stackOfInts.suffix(2)
// suffix contains 20 and 30

在上面的例子中,Stack的Suffix关联类型也是Stack,因此Stack上的Suffix操作返回另一个堆栈。或者,符合SuffixableContainer的类型可以具有与自身不同的Suffix类型,这意味着Suffix操作可以返回不同的类型。例如,以下是对非泛型IntStack类型的扩展,它添加了suffexablecontainer一致性,使用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
    }
    // Inferred that Suffix is Stack<Int>.
}

9 泛型where子句

类型约束(如类型约束中所述)使您能够定义与泛型函数、下标或类型关联的类型参数的要求。

定义关联类型的需求也很有用。您可以通过定义一个泛型where子句来实现这一点。泛型where子句使您能够要求关联类型必须符合特定协议,或者某些类型参数和关联类型必须相同。泛型where子句以where关键字开头,后跟关联类型的约束或类型与关联类型之间的相等关系。在类型或函数体的左大括号之前编写一个泛型where子句。

下面的示例定义了一个名为allItemsMatch的通用函数,该函数检查两个容器实例是否以相同的顺序包含相同的项。如果所有项匹配,则函数返回布尔值true;如果不匹配,则返回false值。

要检查的两个容器不必是同一类型的容器(尽管它们可以),但它们必须包含相同类型的项目。此要求通过类型约束和通用where子句的组合来表示:

func allItemsMatch<C1: Container, C2: Container>
    (_ someContainer: C1, _ anotherContainer: C2) -> Bool
    where C1.Item == C2.Item, C1.Item: Equatable {

        // Check that both containers contain the same number of items.
        if someContainer.count != anotherContainer.count {
            return false
        }

        // Check each pair of items to see if they're equivalent.
        for i in 0..<someContainer.count {
            if someContainer[i] != anotherContainer[i] {
                return false
            }
        }

        // All items match, so return true.
        return true
}

此函数接受两个参数,分别称为someContainer和anotherContainer。someContainer参数的类型为C1,另一个容器参数的类型为C2。C1和C2都是调用函数时要确定的两个容器类型的类型参数。

函数的两个类型参数有以下要求:

  • C1必须符合容器协议(写为C1:Container)。
  • C2还必须符合容器协议(写为C2:Container)。
  • C1的项必须与C2的项相同(写为C1.Item==C2.Item)。
  • C1的项必须符合相等协议(写为C1.Item: Equatable)。

第一个和第二个需求在函数的类型参数列表中定义,第三个和第四个需求在函数的泛型where子句中定义。

这些要求意味着:

  • someContainer是C1类型的容器。
  • anotherContainer是C2类型的容器。
  • someContainer和anotherContainer包含相同类型的项目。
  • someContainer中的项目可以用不相等运算符(!=)看看它们是否彼此不同。

第三个和第四个要求结合起来意味着另一个容器中的项目也可以用!=运算符,因为它们与someContainer中的项的类型完全相同。 这些要求使allItemsMatch(::)函数能够比较两个容器,即使它们是不同的容器类型。

allItemsMatch(::)函数首先检查两个容器是否包含相同数量的项。如果它们包含不同数量的项,则无法进行匹配,函数将返回false。 完成此检查后,函数使用for in循环和半开范围运算符(…<)迭代someContainer中的所有项。对于每个项,函数检查来自某个容器的项是否与另一个容器中的相应项不相等。如果这两个项不相等,则两个容器不匹配,函数返回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.")
}
// Prints "All items match."

上面的示例创建一个堆栈实例来存储字符串值,并将三个字符串推送到堆栈上。该示例还创建了一个数组实例,该实例使用与堆栈相同的三个字符串的数组文本初始化。即使堆栈和数组的类型不同,它们都符合容器协议,并且都包含相同类型的值。因此,可以使用这两个容器作为参数调用allItemsMatch(::)函数。在上面的示例中,allItemsMatch(::)函数正确地报告两个容器中的所有项都匹配。

10 扩展泛型where子句

也可以使用泛型where子句作为扩展的一部分。下面的示例扩展了前面示例中的泛型堆栈结构,以添加一个isTop(:)方法。

extension Stack where Element: Equatable {
    func isTop(_ item: Element) -> Bool {
        guard let topItem = items.last else {
            return false
        }
        return topItem == item
    }
}

首先将isTop(_:)方法与给定的item()进行比较。如果您试图在没有泛型where子句的情况下执行此操作,则会遇到一个问题:isTop(:)的实现使用==运算符,但堆栈的定义不要求其项是相等的,因此使用==运算符会导致编译时错误。使用泛型where子句可以向扩展添加新的需求,这样扩展只在堆栈中的项相等时才添加isTop(:)方法。

以下是isTop(:)方法的实际操作:

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

如果您尝试在元素不相等的堆栈上调用isTop(:)方法,您将得到一个编译时错误。

struct NotEquatable { }
var notEquatableStack = Stack<NotEquatable>()
let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)
notEquatableStack.isTop(notEquatableValue)  // Error

可以使用带有协议扩展名的泛型where子句。下面的示例扩展了前面示例中的容器协议,以添加startsWith(:)方法。

extension Container where Item: Equatable {
    func startsWith(_ item: Item) -> Bool {
        return count >= 1 && self[0] == item
    }
}

startsWith(:)方法首先确保容器至少有一个项,然后检查容器中的第一项是否与给定项匹配。这个新的startsWith(:)方法可以用于任何符合容器协议的类型,包括上面使用的堆栈和数组,只要容器的项是相等的。

if [9, 9, 9].startsWith(42) {
    print("Starts with 42.")
} else {
    print("Starts with something else.")
}
// Prints "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)
    }
}
print([1260.0, 1200.0, 98.6, 37.0].average())
// Prints "648.9"

此示例向项类型为Double的容器添加average()方法。它迭代容器中的项以将它们相加,然后除以容器的计数来计算平均值。它显式地将count从Int转换为Double,以便能够进行浮点除法。

您可以在作为扩展的一部分的泛型where子句中包含多个需求,就像在别处编写的泛型where子句一样。用逗号分隔列表中的每个需求。

11 上下文相关的泛型Where子句

当您已经在泛型类型的上下文中工作时,您可以编写一个泛型where子句作为声明的一部分,该声明没有自己的泛型类型约束。例如,可以对泛型类型的下标或泛型类型扩展中的方法编写泛型where子句。容器结构是泛型的,下面示例中的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())
// Prints "648.75"
print(numbers.endsWith(37))
// Prints "true"

此示例在项为整数时向容器中添加average()方法,在项可相等时添加endsWith(:)方法。这两个函数都包含一个泛型where子句,该子句将类型约束添加到容器原始声明的泛型项类型参数中。

如果您想在不使用上下文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子句的示例版本中,average()和endsWith(:)的实现都在同一个扩展中,因为每个方法的泛型where子句都声明了使该方法可用所需满足的需求。将这些需求移到扩展的泛型where子句中可以使方法在相同的情况下可用,但是每个需求需要一个扩展。

12 关联类型的泛型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
}

迭代器上的泛型where子句要求迭代器必须遍历与容器项具有相同项类型的元素,而不考虑迭代器的类型。函数的作用是:提供对容器迭代器的访问。

对于从另一个协议继承的协议,可以通过在协议声明中包含泛型where子句向继承的关联类型添加约束。例如,以下代码声明了一个ComparableContainer协议,该协议要求项符合Comparable:

protocol ComparableContainer: Container where Item: Comparable { }

13 泛型下标

下标可以是泛型的,并且可以包括泛型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
    }
}

容器协议的这个扩展添加了一个下标,该下标接受一系列索引,并返回一个数组,其中包含每个给定索引中的项。通用下标的约束如下:

  • 尖括号中的泛型参数索引必须是符合标准库中序列协议的类型。
  • 下标接受一个参数index,它是该索引类型的一个实例。
  • 泛型where子句要求序列的迭代器必须遍历Int类型的元素。这可以确保序列中的索引与用于容器的索引的类型相同。

综合起来,这些约束意味着为index参数传递的值是一个整数序列。

参考文档:Swift - Generics