聊一聊Swift泛型

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

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

泛型解决的问题

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

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

函数将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值一起使用。如果您想交换两个字符串值,或者两个双值,您必须编写更多的函数,例如swaptwostring和swaptwodouble函数,如下所示:

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、swaptwostrin和swaptwodouble函数的主体是相同的。唯一的区别是它们接受的值的类型(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
}
复制代码

swapTwoValues函数的主体与swapTwoInts函数的主体相同。然而,swapTwoValues的第一行与swaptwoint略有不同。以下是第一行的比较:

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) ()。括号告诉Swift, T是swapTwoValues(::)函数定义中的占位符类型名。因为T是一个占位符,所以Swift并不寻找一个名为T的实际类型。

现在可以像调用swaptwoint一样调用swapTwoValues(::)函数,但它可以传递任意类型的两个值,只要这两个值彼此具有相同的类型。每次调用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(_:_:)函数,而不是提供自己的实现。
复制代码

类型参数

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

一旦指定了类型参数,就可以使用它定义函数参数的类型(例如swapTwoValues(::)函数的a和b参数),或者作为函数的返回类型,或者作为函数体中的类型注释。在每种情况下,每当调用函数时,类型参数都会替换为实际类型。(在上面的swapTwoValues(::)示例中,第一次调用函数时用Int替换T,第二次调用时用String替换T)。

您可以通过在尖括号中编写多个类型参数名(以逗号分隔)来提供多个类型参数。

命名类型参数

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

请注意
始终使用大写的驼峰类型参数名(例如TMyTypeParameter)来表示它们是类型的占位符,而不是值。
复制代码

泛型类型

除了泛型函数之外,Swift还允许您定义自己的泛型类型。这些自定义类、结构和枚举可以使用任何类型,类似于数组和Dictionary。

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

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

下图显示了栈的push和pop行为:

1.当前堆栈上有三个值。

2.第四个值被推到堆栈的顶部。

3.堆栈现在包含四个值,最近的值位于顶部。

4.弹出堆栈中的顶部Item。

5.弹出一个值后,堆栈再次包含三个值。

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

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

该结构使用名为items的数组属性在堆栈中存储值。Stack提供了两个方法push和pop,用于在栈上和栈外推送和弹出值。这些方法被标记为mutating,因为它们需要修改(或改变)结构的items数组。

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

这是相同代码的泛型版本:

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

注意,Stack的泛型版本与非泛型版本本质上是相同的,但是类型参数是Element,而不是Int的实际类型。

元素为稍后提供的类型定义占位符名称。这种未来类型可以在结构定义的任何位置作为元素引用。在本例中,元素被用作三个位置的占位符:

  • 要创建一个名为items的属性,该属性使用type Element来初始化空数组

  • 要指定push(_:)方法有一个名为item的参数,该参数必须是type Element

  • 指定pop()方法返回的值为type Element的值

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

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

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

下面是stackofstrings将这四个值(从下到上分别对应西班牙语的1,2,3,4)放入堆栈后的样子:

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

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

下面是弹出栈顶值后栈的样子:

泛型类型拓展

当拓展泛型类型时,不提供类型参数列表作为extension定义的一部分。相反,extension体中可以使用来自原始类型定义的类型参数列表,并且原始类型参数名称用于引用来自原始定义的类型参数。

下面的例子拓展了泛型堆栈类型,添加了一个只读的计算属性topItem,它返回堆栈上的顶Item,而不从堆栈中弹出它:

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

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

注意,这个extension没有定义类型参数列表。相反,在extension中使用堆栈类型的现有类型参数名称Element来指示topItem computed属性的可选类型。

topItem computed属性现在可以与任何堆栈实例一起使用,在不删除它的情况下访问和查询它的topItem。

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

泛型类型的extension还可以包括extension类型的实例必须满足的需求,以便获得新功能,如下面的泛型Where子句的extension中所述。

  • 类型约束

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

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

这一要求是通过对Dictionary键类型的类型约束来实现的,它指定键类型必须符合Hashable协议,这是Swift标准库中定义的一种特殊协议。所有Swift的基本类型(如String、Int、Double和Bool)在默认情况下都是可hashable的。

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

类型约束的语法

通过在类型参数(T)的名称后面放置一个类或协议约束(以冒号分隔)作为类型参数列表的一部分来编写类型约束。泛型函数的类型约束的基本语法如下所示(尽管泛型函数的语法是相同的):

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

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

有效的类型约束

这是一个名为findIndex(ofString:in:)的非泛型函数,它被赋予一个要查找的字符串值和一个字符串值数组,可以在其中找到它。函数的作用是:返回一个可选的Int值,如果找到了数组中第一个匹配的字符串,它将作为索引;

func findIndex(ofString valueToFind: String, in array: [String]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}
// enumerated常用来代替for循环遍历数组取下标与值
复制代码

函数的作用是:查找字符串数组中的字符串值。

let strings = ["simon", "mike", "kimi", "sunny", "mandy", "gunna", "cynthia""vince""ben"]
if let foundIndex = findIndex(ofString: "mandy", in: strings) {
    print("The index of mandy is \(foundIndex)")
}
// Prints "The index of mandy is 4"
复制代码

但是,在数组中查找值索引的原则并不只适用于字符串。您可以编写与泛型函数相同的功能,方法是用某种类型的T值替换任何提到的字符串。

下面是如何编写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:)函数一起使用,因为它保证支持equal to操作符。为了表达这个事实,当你定义函数时,你写一个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,这意味着“任何类型的T都符合Equatable协议”。

函数的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
复制代码

关联类型

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

Associated Types in Action

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

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

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

  • 必须能够使用append(_:)方法向容器添加新Item。

  • 必须能够通过返回Int值的count属性访问容器中Item的计数。

  • 必须能够使用下标检索容器中的每一Item,下标接受Int索引值。

该协议没有指定容器中的Item应该如何存储或允许存储什么类型的Item。协议只指定任何类型必须提供的三个功能位,以便将其视为容器。符合标准的类型可以提供额外的功能,只要它满足这三个需求。

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

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

为了实现这一点,容器协议声明了一个名为Item的关联类型,它被写成associatedtype Item。协议没有定义什么Item——任何符合要求的类型都可以提供这些信息。尽管如此,Item目别名(typealias)提供了一种方法来引用容器 中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指定对于容器的这个实现,使用的合适的Item是Int类型,typealias Item = Int的定义将抽象的类型转换为容器协议的这个实现的具体的Int类型。

由于Swift的类型推断,实际上不需要将Int的具体Item声明为IntStack定义的一部分。由于IntStack符合容器协议的所有要求,Swift只需查看append(_:)方法的Item参数的类型和下标的返回类型,就可以推断出要使用的适当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]
    }
}
复制代码

这一次,类型参数元素用作append(_:)方法的Item参数的类型和下标的返回类型。因此,Swift可以推断元素是作为该特定容器Item使用的适当类型。

拓展现有类型以指定关联类型

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

Swift的数组类型已经提供了一个append(_:)方法、一个count属性和一个下标,下标带有一个Int索引来检索它的元素。这三个功能符合容器协议的要求。这意味着只需声明Array采用该协议,就可以拓展Array以符合容器协议。您可以使用一个空extension来实现这一点,正如在声明使用extension采用协议时所述:

extension Array: Container {}
复制代码

Array现有的append(_:)方法和下标使Swift能够推断出要为Item使用的适当类型,就像上面的泛型堆栈类型一样。定义此extension之后,可以使用任何数组作为容器。

给关联类型添加约束

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

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

为了符合这个版本的容器,容器的Item类型必须符合Equatable协议。

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

协议可以作为其自身需求的一部分出现。例如,这里有一个协议改进了容器协议,添加了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子句的关联类型中讨论。

这里是一个堆栈类型的extension,来自上面闭包的强引用循环,增加了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类型的extension,添加了Suffixablecontainer一致性,使用Stack作为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>.
}
复制代码

泛型where子句

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

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

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

要检查的两个容器不必是相同类型的容器(虽然可以是相同类型的容器),但是它们必须包含相同类型的Item。这一要求是通过类型约束和泛型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和另一个容器。someContainer参数类型为C1,另一个容器参数类型为C2。C1和C2都是调用函数时要确定的两种容器类型的类型参数。

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

  • C1必须符合容器协议(写为C1: Container)。

  • C2还必须符合容器协议(写为C2: Container)。

  • C1的Item必须与C2的Item相同(写为C1)。Item= = C2.Item)。

  • C1Item必须符合Equatable协议(以C1的形式编写)。Item:Equatable)。

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

这些要求是:

  • someContainer是C1类型的容器。

  • 另一个容器是C2类型的容器。

  • 某个容器和另一个容器包含相同类型的Item。

  • 可以使用not equal操作符(!=)检查someContainer中的Item,看看它们彼此是否不同。

第三和第四个需求组合在一起意味着可以使用!=操作符检查另一个容器中的Item,因为它们与someContainer中的Item完全相同

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

函数的作用是:首先检查两个容器是否包含相同数量的Item。如果它们包含不同数量的Item,则无法匹配,函数返回false。

检查完成后,该函数使用for-in循环和半开放范围操作符(..<)遍历someContainer中的所有Item。对于每个Item,函数检查来自someContainer的Item是否与另一个容器中的对应Item不相等。如果两Item不相等,则两个容器不匹配,函数返回false。

如果循环结束时没有发现不匹配,则两个容器匹配,函数返回true。

下面是allItemsMatch(::)函数实际运行的样子:

var stackOfStrings = Stack<String>()
stackOfStrings.push("one")
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(::)函数正确地报告了两个容器中的所有Item

泛型Where子句的拓展

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

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

这个新的isTop(:)方法首先检查堆栈是否为空,然后将给定的Item与堆栈的最上面的Item进行比较。如果您尝试在没有泛型where子句的情况下执行此操作,那么就会遇到一个问题:isTop(:)的实现使用==操作符,但是堆栈的定义并不要求它的Item是相等的,因此使用==操作符会导致编译时错误。使用泛型where子句可以向extension添加新需求,因此只有当堆栈中的Item相等时,extension才会添加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
复制代码

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

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

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

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())
// Prints "648.9"
复制代码

这个例子向Item类型为Double的容器添加了一个average()方法。它遍历容器中的Item并将它们相加,然后除以容器的计数来计算平均值。它显式地将计数从Int转换为Double,以便能够进行浮点除法。

可以在extension的泛型where子句中包含多个需求,就像可以在其他地方编写泛型where子句一样。用逗号分隔列表中的每个需求。

与泛型Where子句关联的类型

可以在关联类型上包含泛型where子句。例如,假设您想要创建一个包含Iterator的容器版本,就像序列协议在标准库中使用的那样。你可以这样写:

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必须遍历与容器Item类型相同的Item类型的元素,而不管Iterator的类型如何。函数的作用是:提供对容器Iterator的访问。

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

protocol ComparableContainer: Container where Item: Comparable { }
复制代码

泛型下标

下标可以是泛型的,它们可以包含泛型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
    }
}
复制代码

容器协议的这个extension添加了一个下标,它接受一系列索引,并返回一个数组,其中包含每个给定索引上的Item。这个泛型下标的约束条件如下:

  • <>中的泛型Indices必须是符合标准库中的Sequence类型。

  • 下标接受单个参数indices(index的复数形式),这是该索引类型的一个实例。

  • 泛型where子句要求序列的Iterator必须遍历Int类型的元素,这确保序列中的索引与容器中使用的索引类型相同。

总的来说,这些约束意味着为索引参数传递的值是一个整数序列。

[本文英文地址swift 5]docs.swift.org/swift-book/…

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值