Swift学习笔记 (三十五) 泛型(下)

关联类型

定义一个协议时,声明一个或多个关联类型作为协议定义的一部分将会非常有用。关联类型为协议中的某个类型提供了一个占位

符名称,其代表的实际类型在协议被遵循时才会被指定。关联类型通过 associatedtype 关键字来指定。

 

关联类型实践

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

protocol Container {

    associatedtype Item

    mutating func append(_ item: Item)

    var count: Int { get }

    subscript(i: Int) -> Item { get }

}

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

    1.必须可以通过 append(_:) 方法添加一个新元素到容器里。

    2.必须可以通过 count 属性获取容器中元素的数量,并返回一个 Int 值。

    3.必须可以通过索引值类型为 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 在实现 Container 的要求时,指定 Item 为 Int 类型,即 typealias Item = Int ,从而将 Container 协议中抽象的 

Item 类型转换为具体的 Int 类型。

由于 Swift 的类型推断,实际上在 IntStack 的定义中不需要声明 Item 为 Int 。因为 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 具体类型。定义了这个扩展后,你可以将任意 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 是一个关联类型,就像上边例子中 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

在上面的例子中, Suffix 是 Stack 的关联类型,也是 Stack ,所以 Stack 的后缀运算返回另一个 Stack。另外,遵循 

SuffixableContainer 的类型可以拥有一个与它自己不同的 Suffix 类型——也就是说后缀运算可以返回不同的类型。⽐如说,这⾥

有一个非泛型 IntStack 类型的扩展,它遵循了 SuffixableContainer 协议,使⽤ Stack<Int> 作为它的后缀类型而不是 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

}

这个函数接受 someContainer 和 anotherContainer 两个参数。参数 someContainer 的类型为 C1 ,参数 anotherContainer 的类

型为 C2 。 C1 和 C2 是容器的两个占位类型参数,函数被调用时才能确定它们的具体类型。 这个函数的类型参数列表还定义了

对两个类型参数的要求:

    1.C1 必须符合 Container 协议(写作 C1: Container )。

    2.C2 必须符合 Container 协议(写作 C2: Container )。

    3.C1 的 Item 必须和 C2 的 Item 类型相同(写作 C1.Item == C2.Item )。

    4.C1 的 Item 必须符合 Equatable 协议(写作 C1.Item: Equatable )。

前两个要求定义在函数的类型形式参数列表里,后两个要求定义在了函数的泛型 where 分句中。 这些要求意味着:

    1.someContainer 是一个 C1 类型的容器。

    2.anotherContainer 是一个 C2 类型的容器。

    3.someContainer 和 anotherContainer 包含相同类型的元素。

    4.someContainer 中的元素可以通过不等于操作符(!=)来检查它们是否相同。

第三个和第四个要求结合起来意味着 anotherContainer 中的元素也可以通过 != 操作符来⽐较,因为它们和 someContainer 中的

元素类型相同。

这些要求让 allItemsMatch(_:_:) 函数能够比较两个容器,即使它们的容器类型不同。

allItemsMatch(_:_:) 函数首先检查两个容器元素个数是否相同,如果元素个数不同,那么一定不匹配,函数就会返回 false 。

进⾏这项检查之后,通过 for-in 循环和半闭区间操作符( ..< )来迭代每个元素,检查 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(_:) ⽅法首先确保容器至少有一个元素,然后检查容器中的第一个元素是否与给定的元素相等。任何符合 

Container 协议的类型都可以使用这个新的 startsWith(_:) 方法,包括上面使用的栈和数组,只要容器的元素是符合 Equatable 协

议的。

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”

此示例将一个 average() 方法添加到 Item 类型为 Double 的容器中。此⽅法遍历容器中的元素将其累加,并除以容器的数量计算

平均值。它将数量从 Int 转换为 Double 确保能够进⾏浮点除法。

就像可以在其他地方写泛型 where ⼦句一样,你可以在一个泛型 where ⼦句中包含多个条件作为扩展的一部分。⽤逗号分隔列

表中的每个条件。

 

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

你可以在关联类型后⾯加上具有泛型 where 的子句。例如,建立一个包含迭代器( Iterator )的容器,就像是标准库中使用的 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])

        }

    }

}

这个 Container 协议的扩展添加了一个下标方法,接收一个索引的集合,返回每一个索引所在的值的数组。这个泛型下标的约束

如下:

    1.在尖括号中的泛型参数 Indices ,必须是符合标准库中的 Sequence 协议的类型。

    2.下标使用的单一的参数, indices ,必须是 Indices 的实例。

    3.泛型 where 子句要求 Sequence(Indices) 的迭代器,其所有的元素都是 Int 类型。这样就能确保在序列 ( Sequence )中的索

        引和容器( Container )⾥面的索引类型是一致的。

综合一下,这些约束意味着,传入到 indices 下标,是一个整型的序列。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值