Swift learning part 22 - 泛型

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

泛型是 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
}

这个函数使用输入输出参数(inout)来交换 a 和 b 的值。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( _: _: ) 的函数功能都是相同的,唯一不同之处就在于传入的变量类型不同,分别是 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
}

这个函数的泛型版本使用了占位类型名(在这里用字母 T 来表示)来代替实际类型名(例如 Int、String 或 Double)。占位类型名没有指明 T 必须是什么类型,但是它指明了 a 和 b 必须是同一类型 T,无论 T 代表什么类型。只有 swapTwoValues( _: _: ) 函数在调用时,才会根据所传入的实际类型决定 T 所代表的类型。

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

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

var aInt = 3
var bInt = 107
swapTwoValues(&aInt, &bInt)
print("aInt is now \(aInt), bInt is now \(bInt)") // 打印“aInt is now 107, bInt is now 3”

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString 现在是“world”,anotherString 现在是“hello”

注意
上面定义的 swapTwoValues( _: _: ) 函数是受 swap( _: _: ) 函数启发而实现的。后者存在于 Swift 标准库,如果在代码中需要类似 swapTwoValues( _: _: ) 函数的功能,你可以直接使用已存在的 swap( _: _: ) 函数。

类型参数

在上面的 swapTwoValues( _: _: ) 例子中,占位类型 T 是类型参数的一个例子。类型参数指定并命名一个占位类型,并且紧随在函数名后面,使用一对尖括号括起来(例如 )。

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

你可提供多个类型参数,将它们都写在尖括号中,用逗号分开。

命名类型参数

大多数情况下,类型参数具有一个描述性名字,例如 Dictionary<Key, Value> 中的 Key 和 Value,以及 Array 中的 Element,这可以告诉阅读代码的人这些类型参数和泛型函数之间的关系。然而,当它们之间没有有意义的关系时,通常使用单个字母来命名,例如 T、U、V,正如上面演示的 swapTwoValues( _: _: ) 函数中的 T 一样。

注意:请始终使用大写字母开头的驼峰命名法(例如 T 和 MyTypeParameter)来为类型参数命名,以表明它们是占位类型,而不是一个值。

泛型类型

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

这部分内容将向你展示如何编写一个名为 Stack (栈)的泛型集合类型。栈是一系列值的有序集合,和 Array 类似,但它相比 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()
    }
}

这个结构体在栈中使用一个名为 items 的 Array 属性来存储值。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()
    }
}

Stack 基本上和 IntStack 相同,只是用占位类型参数 Element 代替了实际的 Int 类型。这个类型参数包裹在紧随结构体名的一对尖括号里(< Element >)。

Element 为待提供的类型定义了一个占位名。这种待提供的类型可以在结构体的定义中通过 Element 来引用。

在这个例子中,Element 在如下三个地方被用作占位符:

创建 items 属性,使用 Element 类型的空数组对其进行初始化。
指定 push(_: ) 方法的唯一参数 item 的类型必须是 Element 类型。
指定 pop() 方法的返回值类型必须是 Element 类型。

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

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

var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// 栈中现在有 4 个字符串

移除并返回栈顶部的值“cuatro”,即出栈:

let fromTheTop = stackOfStrings.pop()
// fromTheTop 的值为“cuatro”,现在栈中还有 3 个字符串,栈顶的值变为了“tres”

创建一个可以存储多个不同类型的实例的栈:

var stackOfAny = Stack<Any>()
stackOfAny.push("any")
stackOfAny.push(3)
stackOfAny.push(3.14159)

泛型扩展

扩展一个泛型类型的时候,并不需要在扩展的定义中提供类型参数列表。原始类型定义中声明的类型参数列表在扩展中可以直接使用,并且这些来自原始类型中的参数名称会被用作原始定义中类型参数的引用。

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

extension Stack {
    var topItem: Element? {
        return items.isEmpty ? nil : items[items.count - 1]
    }
}
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)的。也就是说,必须有一种方法能够唯一地表示它。Dictionary 的键之所以要是可哈希的,是为了便于检查字典是否已经包含某个特定键的值。若没有这个要求,Dictionary 将无法判断是否可以插入或者替换某个指定键的值,也不能查找到已经存储在字典中的指定键的值。

这个要求通过 Dictionary 键类型上的类型约束实现,它指明了键必须遵循 Swift 标准库中定义的 Hashable 协议。所有 Swift 的基本类型(例如 String、Int、Double 和 Bool)默认都是可哈希的。

当自定义泛型类型时,你可以定义你自己的类型约束,这些约束将提供更为强大的泛型编程能力。像 可哈希(hashable) 这种抽象概念根据它们的概念特征来描述类型,而不是它们的具体类型。

类型约束语法

在一个类型参数名后面放置一个类名或者协议名,并用冒号进行分隔,来定义类型约束,它们将成为类型参数列表的一部分。

对泛型函数添加类型约束的基本语法如下所示(作用于泛型类型时的语法与之相同):

func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
    // 这里是泛型函数的函数体部分
}

上面这个函数有两个类型参数。第一个类型参数 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)")
}
// 打印 “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 标准库中定义了一个 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”。

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

关联类型

定义一个协议时,有的时候声明一个或多个关联类型作为协议定义的一部分将会非常有用。关联类型为协议中的某个类型提供了一个占位符名称,其代表的实际类型在协议被采纳时才会被指定。可以通过 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 的行为都能够正如预期地被执行。

下面定义一个遵循 Container 协议的 DoubleStack 类型:

struct DoubleStack: Container {
    var items = [Double]()
    
    typealias Item = Double
    
    mutating func append(_ item: Double) {
        items.append(item)
    }
    
    var count: Int {
        return items.count
    }
    
    subscript(i: Int) -> Double {
        return items[i]
    }
}

DoubleStack 在实现 Container 的要求时,指定 ItemType 为 Double 类型,即 typealias Item = Double,从而将 Container 协议中抽象的 Item 类型转换为具体的 Double 类型。

由于 Swift 的类型推断,实际上不用在 DoubleStack 的定义中声明 Item 为 Double。因为 DoubleStack 符合 Container 协议的所有要求,Swift 只需通过 append(_: ) 方法的 item 参数类型和下标返回值的类型,就可以推断出 Item 的具体类型。

也可以让泛型结构体遵从 Container 协议:

extension Stack: Container {
    // 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 可以据此推断出 Stack 中的 Element 的类型即是 Container 协议中要求的 Item 的类型。

扩展现有类型来指定关联类型

通过扩展添加协议一致性中描述了如何利用扩展让一个已存在的类型符合一个协议,这包括使用了关联类型的协议。

Swift 的 Array 类型已经提供 append(_: ) 方法,一个 count 属性,以及一个接受 Int 类型索引值的下标用以检索其元素。这三个功能都符合 Container 协议的要求,也就意味着只需简单地声明 Array 采纳该协议就可以扩展 Array,使其遵从 Container 协议。你可以通过一个空扩展来实现这点,正如通过扩展采纳协议中的描述:

extension Array: Container {}

如同上面的泛型 Stack 结构体一样,Array 的 append(_: ) 方法和下标确保了 Swift 可以推断出 Item 的类型。定义了这个扩展后,你可以将任意 Array 当作 Container 来使用。

约束关联类型

你可以在协议里给关联类型添加约束来要求遵循的类型满足约束。例如,下面的代码定义了 AnotherContainer 协议, 要求关联类型 Item 必须遵循 Equatable 协议:

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

为了遵守 AnotherContainer 协议,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 类型相同。

这是上面泛型类型中 AnotherStack 类型的扩展,它遵循了 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

泛型 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
}

这个函数接受 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(_: _: ) 函数的示例:

var stackOfDoubles = DoubleStack()
stackOfDoubles.append(0.1)
stackOfDoubles.append(0.2)
stackOfDoubles.append(0.3)

var arrayOfDoubles = [0.1, 0.2, 0.3]
if allItemsMatch(stackOfDoubles, arrayOfDoubles) {
    print("All items match.")
} else {
    print("Not all items match.")
}
// 打印“All items match.”

具有泛型 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
    }
}

如果尝试不用泛型 where 子句,那么 Stack 的定义就没有要求它的元素符合 Equatable 协议,isTop( _: ) 里面使用了 == 运算符就会导致编译时错误,因此只有当栈中的元素符合 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
    }
}

任何符合 Container 协议的类型都可以使用这个新的 startsWith(_: ) 方法。

var doubleStack = DoubleStack()
doubleStack.append(1.0)
doubleStack.append(0.1)
if doubleStack.startsWith(1.0) {
    print("Starts with 1.0.")
} else {
    print("Starts with something else.")
}
// 打印“Starts with 1.0.”

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(doubleStack.average())
// 打印 0.55

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

可以在关联类型后面加上具有泛型 where 的子句。例如下面例子建立了一个包含迭代器(Iterator)的容器,就像是标准库中使用的 Sequence 协议那样。

protocol IteratorContainer {
    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 子句要求:无论迭代器是什么类型,迭代器中的元素类型必须和容器项目的类型保持一致。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 子句要求 Sequence(Indices)的迭代器,其所有的元素都是 Int 类型。这样就能确保在序列(Sequence)中的索引和容器(Container)里面的索引类型是一致的。

综合一下,这些约束意味着,传入到 indices 下标,是一个整型的序列。
(原来的 Container 协议,subscript 必须是 Int 型的,扩展中新的 subscript,允许下标是一个 Int 型的序列,而非单一的值。)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值