泛型(Generics)

234 阅读27分钟

原文

泛型编码使你可以写灵活,可重用的可以和任何类型使用的函数和类型,满足于你定义的要求。可以写出避免除服的代码,并且简单,抽象的形式表示他的目的。

泛型是swift最强大的特性之一,大多数的swift标准库用泛型编码写的。实际上,你在Language Guide中已经使用了泛型,即使你没有意识到。例如,swift的Array和Dictionary类型都是泛型集合。你可以创建一个含有Int值的数组或者一个含有String值的数组,或者swift中可以创建的其他的类型的数组,没有限制那个类型可以是什么。

泛型解决的问题(The Problem That Generics Solve)

这是一个标准的非泛型的名为swapTwoInts(_:_:)的函数,交换两个Int值:

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

这个函数使用in-out参数来交换a和b的值,像在In-Out Parameters中描述的。

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值。如果你想交换两个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类型变量他们互相交换值。尝试这样做会导致一个编译器错误。

泛型函数(Generic Functions)

泛型函数可以对任何类型有效。这里是上面函数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表示什么。用来替代T的实际的值每次调用swapTwoValues(_:_:)函数的时候确定。

在泛型函数和非泛型函数之间其他的不同是在泛型函数的名称(swapTwoValues(_:_:))后面跟着在角括号中的默认类型名称(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的泛型函数启发,是在swift标准库中,在你的代码中自动为你变成可获取来使用。如果在你的代码中需要swapTwoValues(_:_:)的性能,你可以使用swift的已存在的swap(_:_:)函数而不是提供你自己的实现。

类型参数(Type Parameters)

在上面swapTwoValues(_:_:)例子中,占位类型T是一个类型参数的一个例子。类型参数指定并命名一个占位类型,紧跟着写在函数名的后面,在一对匹配的角括号之间(例如<T>)。

一旦你指定了一个类型参数,你可以使用它来定义一个函数参数的类型(例如swapTwoValues(_:_:)函数的a和b参数),后者作为函数的返回类型,或者作为在函数中的类型注释。每种情况,函数调用的任何时候用一个实际的类型替代类型参数。(在上面的swapTwoValues(_:_:)例子中,在函数第一次调用的时候用Int替代T,在他调用的第二次的时候用String替换。)

你可以通过在角括号中写多个类型参数名称,用逗号分隔来提供多余一个的类型参数。

命名类型参数(Naming Type Parameters)

在大多数情况下,类型参数已经描述了名称,例如在Dictionary<Key,Value>Key和Value和Array<Element>中的Element,告诉读者关于类型参数和泛型类型或者使用它的函数之间的关系。不过,当他们之间没有明确的关系的时候,一般习惯使用单个字母例如T,U,和V来命名他们,例如在上面swapTwoValues(_:_:)函数中的T。

一般给类型参数驼峰命名(例如T和MyTypeParameter)来指定他们是一个类型的占位符,不是值。

泛型类型(Generic Types)

除了泛型函数,swift中你可以定义自己的泛型类型。这是可以和任何类型使用的自定义类,结构体,和枚举,在Array和Dictionary类似的方式。

这一节展示了如何写一个名为Stack的泛型集合类型。一个stack是一个有序的值集合,和array类似,但是有比swift的Array更限制的一组操作。一个array孕育在数组的任何位置掺入和删除新的对象。一个stack,无论如何,只允许新的对象加到集合的尾部(在栈中压入新的值)。相似的,相似的,一个stack允许从集合的尾部移除对象(从栈中推出一个值)。

stack的概念由UINavigationController类用来在它的导航层中模型化视图控制器。调用UINavigationControl类的pushViewController(_:animated:)方法来在导航栈上增加一个新的视图控制器,和它的popViewControllerAnimated(_:)方法类从导航栈中移除一个视图控制器。当你需要一个直接的“last in,first out”方式来管理集合的时候stack是一个很有用的集合模型。

下面的图标展示了一个stack的压入和推出表现:


  1. 在栈中现在有三个值
  2. 第四个值压在栈的顶部
  3. 现在栈有四个值,在顶部是最近的一个
  4. 栈中最上面的对象推出
  5. 推出一个值后,栈再次有三个值

直立式如何写一个非泛型版本的stack,这是一个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类更有用,可以管理任何值的类型的stack。

这是一个相同编码的泛型版本:

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

注意Stack的泛型版本如何在本质上和非泛型版本一样,但是用一个名为Element的类型参数代替一个真实的Int类型。这个类型参数跟在结构体的名称后面写在一对尖括号中(<Element>)。

Element给后面提供的类型定义了一个占位名。在结构体的定义中任何地方像Element一样引用这个将来的类型。这种情况下,Element在三个地方作为占位符使用:

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

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

通过在角括号中写要存储在stack中的类型来创建一个新的Stack实例。例如,创建一个新的strings的新stack,写Stack<String>():

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

这是在stack上压入这四个值之后stackOfStrings看起来的样子:


从stack中推出一个值删除并返回最上面的值,“cuatro”:

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

这是在推出它的顶部值后stack的样子:


扩展一个泛型类型(Extending a Generic Type)

当你扩展一个泛型类型的时候,不用在扩展的定义中提供一个类型参数列表。但是,原始类型定义中的类型参数列表在扩展的主体中可以获得,原始定义中原始类型参数名称用来引用类型参数。

下面的例子扩展了泛型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).")
}
// Prints "The top item on the stack is tres."

泛型类型扩展也可以包含扩展类型的实例为了获得新功能必需满足的条件,像下面Extensions with a Generic Where Clause中描述的。

类型约束(Type Constraints)

swapTwoValues(_:_:_:)函数和Stack类型可以和任何类型一起使用。不过,有时候强制在能和泛型函数和泛型类型一起使用的类型上的进行明确的类型约束很有用。类型约束指定类型参数必需继承自一个指定的类,或者遵循一个特殊的协议或者协议组合。

例如,swift的Dictionary类型在类型上放了一个限制,可以用来为字典作为key的类型。像在Dictionaries中描述的,字典的key的类型必须是Hashable。也就是,他必须提供一个使他独一无二的表示的方式。Dictionary需要他的keys是hasable的来检查它对于一个指定的key是否已经包含了一个值。没有这个要求,Dictionary不能告诉他是否应该插入或者代替一个指定key的值,也不能为已经存在在字典中的给定的key找一个值。

在Dictionary的key类型上的一个类型约束强制了这个要求,指定key类型必需遵循Hashable协议,一个定义在swift标准库中的特殊的协议。所有的基础类型(例如String,Int,Double,和Bool)默认是Hashable的。

当你创建自定义泛型类型的时候你可以定义自己的类型约束,并且约束提供了泛型编码强大的能力。像Hashable的抽象概念描述了他们的概念上的特性的阶段的类型的特性,而不是他们的实体的类型。

类型约束语法(Type Constraint Syntax)

通过在类型参数列表中在类型参数的后面,用冒号分隔放一个单独的类或者协议约束来表示类型约束。下面展示了在一个泛型函数上的类型约束的基础语法(然而对泛型类型的语法是一样的):

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

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

使用中的类型约束(Type Constraints in Action)

这里是一个名为findInde(ofString:in:)的非泛型函数,给了它一个要查找的String值和一个在其中查找它的String值的数组。findIndex(ofString:in:)函数返回一个可选的Int值,如果它存在的话会是在数组中第一个查找到的string的下标,或者如果找不到的话是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:)可以用来在一个string数组中查找一个string值:

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"

在数组中查找一个值的下标的原则不止对strings可用,不过。通过用类型T替代的值代替任何提到的strings可以写用一个泛型函数写相同的功能。

这是如何去写你可能期望的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中每个类型都能用相等号来比较。如果你创建你自己的类或者结构体来表示复杂的数据模型,例如,那么对于那个类或者结构体的“equal to”的意义不是swift能为你猜到的。为此,不可能保证这个代码会对每一个可能的类型T使用,并且当你尝试编译这个代码的时候会报一个恰当的错误。

不过,还有一线希望。swift标准库定义了一个名为Equatable的协议,需要任何遵循的类型来实现等于号(==)和不等于号(!=)来比较这个类型的两个值。swift的全部标准类型自动支持这个Equatable协议。

任何事Equable的类型可以安全的和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])
// 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

关联类型(Associated Types)

当定义一个协议的时候,有时候需要在协议的定义中声明一个或者多个关联的类型。一个关联类型给一个在协议中用到的类型一个占位名字。关联类型使用的真正的类型知道协议采用之前都是不确定的。关联类型用associatedtype关键字指定。

使用中的关联类型(Associated Type in Action)

这是一个名为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的下标返回的类型会是和容器元素的类型一样的类型。

为了满足这些,Container协议声明了一个名为Item的关联类型,写成associatedtype Item。协议没有定义item是什么,这个信息留给任何遵循的类型来提供。不过,Item别名提供了一个指向一个容器中的对象的类型的方式,并且为和append(_:)方法和下标一起使用定义了一个类型,来确保强制的要求任何容器的期望的表现。

这里是上面的Generic Types非泛型IntStack类型的一个版本,改为遵循Container协议:

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类型实现了全部的三个Container协议的要求,每一个都把IntStack类型的部分已存在的功能包装起来满足这些要求。

除此之外,IntStack指定对于这个Container的实现,用的合适的Item是Int类型。typelias Item = Int定义为这个Container协议的实现将抽象的Item类型转化为真实的Int类型。

感谢swift的类型推导,实际上不需要在IntStack的定义中声明一个实体Item是Int。因为IntStack遵循全部的Container协议的要求,swift可以推导使用合适的Item,简单的通过查看append(_:)方法的item参数和下标的返回类型。实际上,如果你从上面代码中删除了typealias Item = Int一行,所有的事情仍然运行,因为很清楚Item要使用什么类型。

你也可以让泛型Stack类型遵循Container协议:

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]
    }
}

这个时候,Element类型参数用在append(_:)方法的item参数的类型和下标的返回类型。因此swift推导出Element这个特定的容器的用作Item的合适的类型。

扩展一个已存在的类型来指定一个关联类型(Extending an Existing Type to Specify an Associated Type)

你可以扩展一个已存在的类型来增加协议的遵循,像在Adding Protocol Conformance with an Extension中描述的。这包括一个有一个关联类型的协议。

swift的array类型已经提供了一个append(_:)方法,一个count属性,和一个下标,用Int索引获取他的元素。这三个功能匹配container协议的要求。这意味着你可以简单的声明array采用协议来扩展array使他遵循Container协议。使用一个空的扩展实现它,像Declaring Protocol Adoption with an Extension中描述的:

extension Array: Container {}

数组的已存在的append(_:)方法和下标使swift推导出给Item使用的合适的类型,就像上面的泛型Stack类型。在定义这个扩展之后,你可以像一个Container一样使用array。

给关联的类型增加约束(Adding Constraints  to an Associated Type)

你可以在一个协议中给关联类型增加类型约束来要求遵循的类型满足这些约束。例如,下面的代码定一个Container的要求容器中的对象是Equatable的版本:

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

遵循这个版本的Container,容器的Item类型不得不遵循Equatable协议。

在它的关联类型约束中使用协议(Using a Protocol in Its Associated Type‘s Constraints)

一个协议可以出现在他自己的要求中。例如,这里的协议重新定义了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从句,在下面的Associated Types with a Generic Where Clause中描述。

这里是一个上面Generic TypesStack类型的扩展,增加对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操作返回另一个Stack。不过,遵循SuffixableContainer的类型可以有一个和他自己不同的Suffix类型--意味着suffix操作可以返回一个不同的类型。例如,这里是一个对非泛型增加SuffixableContainer遵循的IntStack类型的扩展,使用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
    }
    // Inferred that Suffix is Stack<Int>.
}

泛型where从句(Generic Where Clauses)

类型约束,像在Type Constraints中描述的,让你可以在和泛型函数,下标,或者类型的关联类型参数上定义条件。

也可以用来为关联类型定义要求。通过定义一个泛型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 {

        // 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类型的,anotherContainer参数时C2类型的。两个C1和C2是两个在函数调用的时候确定的容器类型的类型参数。

下面的要求放在番薯的两个类型参数:

  • C1必需遵守Container协议(写成C1:Container)
  • C2也必须遵守Container协议(写作C2:Container)
  • C1的Item必需和C2的Item一样(写作C1.Item==C2.Item)。
  • C1的Item必须遵守Equable协议(写作C1。Item:Equatable)

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

协议要求意味着:

  • someCOntainer是C1类型的Container
  • anotherContainer是C2类型的Container
  • someContainer和anotherContainer包含一样类型的item
  • someContainer中的items可以用不等号来查看他们是否不一样

第三个和第四个要求结合起来意味着anotherContainer也可以用!=检查,因为他们和someContainer中的items完全一样的类型。

这些要求确保allItemsMatch(_:_:)函数比较两个container,即使他们是不同的container类型。

allItemsMatch(_:_:)函数开始先检查两个containers包含相同数目的items。如果它们包含不同数目的items,他们不可能匹配,函数返回false。

标记完这个检查后,函数用一个for in循环和半开区间操作符来遍历someContainer中的全部的对象。对每一个对象,函数检查someContainer中的对象是否和anotherContainer中对应的对象相等。如果两个对象是不相等的,两个container不匹配,函数返回false。

如果循环没有找到不匹配结束了,两个container匹配,函数返回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."

上面的例子创建了一个Stack实例来存储String值,将三个strings压入栈中。该例子也创建了一个用包含和stack一样的三个字符串的字面量数组初始化的Array实例。即使stack和array是不同的类型,他们都遵循Container协议,两个包含相同类型的值。所以你可以把两个container作为参数调用allItemMatch(_:_:)函数,allItemsMatch(_:_:)函数正确的返回在两个containers中的全部对象匹配。

带泛型where从句的扩展(Extensions with a Generic Where Clause)

你也可以在扩展中使用泛型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(_:)方法首先检查stack不是空的,然后把stack的最上层的对象和给定的对象进行对比。如果你尝试不用泛型where从句实现这点,会有一个问题:isTop(_:)的实现使用==操作符,但是Stack的定义不需要他的对象是Equatable,所以使用==操作符导致一个编译错误。使用一个泛型where从句使你给扩展加一个新的条件,只有当stack中的items是Equatable的时候扩展添加isTop(_:)方法。

这里是isTop(_:)方法在使用中的样子:

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

如果你尝试在一个他的元素不是Equatable的stack上调用isTop(_:)方法,将会看到一个编译错误:

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

可以对协议的扩展使用泛型where从句。下面的例子扩展上面例子中的Container协议来增加一个StartsWith(_:)方法。

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

startsWith(_:_)方法首先确定容器至少有一个对象,然后检查容器中的第一个对象是否匹配给定的对象。这个新的startsWith(_:)方法可以用在任何遵循Container协议的类型,包括上面使用的stacks和arrays,只要容器的items是Equatable的。

if [9, 9, 9].startsWith(42) {
    print("Starts with 42.")
} else {
    print("Starts with something else.")
}
// Prints "Starts with something else."

上面的泛型where从句要求Item遵循一个协议,但是你也可以写一个要求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())
// Prints "648.9"

例子给Item类型是Double的容器增加一个average()方法。遍历容器中的items来把它们加起来,再除以容器的count来计算平均值。它把count从Int转化为Double使其可以做浮点型除法。

可以在一个泛型where从句中包括多个请求,就像你能在任何地方写泛型where从句。用逗号分隔列表中的要求。

带泛型where从句的关联类型(Associated Types with a Generic Where Clause)

在一个关联类型上包含一个泛型where从句。例如,假设你要生成一个包含一个iterator的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从句要求iterator必需遍历像container的items一样的类型元素,不过iterator的类型。makeIterator()函数提供访问容器的itertor。

对于从其他协议继承的协议,通过在协议声明中增加泛型where从句给继承的关联类型增加约束。例如,下面的代码声明一个需要Item遵循Comparable的ComparableContainer协议:

protocol ComparableContainer: Container where Item: Comparable { }

泛型下标(Generic Subscripts)

下标可以是泛型的,他们可以包含韩星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的iterator一定要遍历Int类型的元素。这确保了序列中的索引和在容器中使用的索引是一样的类型。

结合起来,这些约束意味着传给Indices参数的值是一个整型的序列。