Swift泛型

目录

引入

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

泛型是 Swift 最强大的功能之一, Swift 标准库的大部分内容都是使用泛型代码构建的。

泛型解决的问题

下面是非泛型函数。

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

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"

这个函数只能交换Int值, 如果要交换字符串或者浮点数就需要在写下面的函数。

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
}

这些函数主体是相同的, 唯一的不同是操作的值的类型。 泛型可以编写更加灵活的通用函数。

泛型函数

泛型函数可以处理任何类型。

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

泛型函数使用了占位符名称(这里是T), 而不是具体的类型名。 占位符名称没有说明具体类型, 但是说明了a和b必须是相同的T类型, 无论T代表什么。 每次调用泛型函数时, 就会实际确定T的类型。

泛型函数与非泛型函数的另一个区别是: 泛型函数名称右边有一对尖括号,里面是占位符名称。 Swift 由此知道T是占位符, 不会查找T的实际类型。

现在可以传入任意两个类型的值, 只要类型相同, 就可以交换。

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"

类型参数

上面的占位符类型T就是类型参数的一个例子。 类型参数指定并命名一个占位符类型, 紧跟在函数名后, 写在一对尖括号中。

指定类型参数后, 可以用它定义参数类型, 或者函数的返回类型, 或者在函数主体中做类型注解。 只要函数被调用, 类型参数才会被实际类型替换。

可以在尖括号内写入多个类型参数名, 中间使用逗号分隔。

命名类型参数

大多数情况下, 类型参数都有描述性名称。 例如:

  • Dictionary <Key, Value>
  • Array <Element> 中的 Element

这些名称会告诉读者类型参数与通用类型或函数之间的关系。 然而, 当它们之间没有有意义的关系时, 传统的做法是使用 T、U 和 V 等单字母来命名它们。

泛型类型

除了泛型函数, Swift还允许定义自己的泛型类型。 这些是自定义类、 结构和枚举, 可以与任何类型一起使用, 与 Array 和 Dictionary 的使用方式类似。

下面是非泛型的Stack写法

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

下面是泛型版本

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

泛型版本增加了Element类型参数, 写在结构名称后面的尖括号中, 在结构主体中就可以使用 Element替代固定类型了。

现在可以使用泛型结构创建任意类型的Stack, 需要在尖括号中写明类型。

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

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

扩展泛型

扩展泛型时, 不在扩展定义中提供类型参数列表。 相反, 原始类型定义中的类型参数列表可以在扩 展的正文中使用, 原始类型参数名称用于引用原始定义中的类型参数。

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).")
}
// Prints "The top item on the stack is tres."

扩展没有定义类型参数列表。 Stack现有的参数名称Element可以直接使用。

类型限制

泛型函数和类型可用于任何类型。 不过有时需要对类型执行某些约束, 类型约束可以规定类型参数 必需继承自特定的类, 或符合特定的协议。

类型约束语法

编写类型约束时, 可在类型参数名称后放置单个类或协议约束, 用冒号分隔, 作为类型参数列表 的一部分。

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

示例函数中, 参数T有类型约束, 要求是SomeClass的子类; 类型U的约束是符合SomeProtocol协议。

类型约束操作

下面的函数查找字符串在数组的索引。

func findIndex(ofString valueToFind: String, in array: [String]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

使用方法

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"

编写为泛型函数

func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

上面的代码会出现编译问题。 因为if会对两个值就行相等比较, 不是所有类型都支持等于运算。 所以我们需要修改泛型函数, 让类型T满足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
}

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

关联类型

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

关联类型应用

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

Container协议定义了三种必要功能:

  • 必须能够通过append方法添加新项目
  • 必须能够返回Int值的count属性
  • 必须可以通过包含Int值的下标检索每个项目

为了定义这些要求, Container协议需要一种方法引用容器将容纳的元素的类型, 而不需要知道 具体类型是什么。 为了实现这个目标, 协议声明了Item关联类型, 写作 associatedtype Item

协议没有定义Item是什么, 这留给任何符合要求的类型来提供。

下面的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]
    }
}

还可以编写泛型Stack支持容器协议。

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

为关联类型添加约束

可以在协议中为关联类型添加类型约束, 以要求符合要求的类型满足这些约束。

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

泛型Where子句

类型约束可以定义与泛型函数、下标或类型相关的类型参数要求。

定义关联类型的要求也很有用。 你可以通过定义泛型 where 子句来实现这一点。 通过通用 where 子句, 可以要求关联类型必须符合特定协议, 或要求特定类型参数和关联类型必须相同。 通用 where 子句以 where 关键字开头, 然后是关联类型的约束或类型与关联类型之间的相等关系。 通用 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
}

使用方式

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."

泛型where子句的扩展

你还可以使用泛型where子句作为扩展的一部分。

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

这样可以添加一个新要求: 只有当Stack中的元素符合Equatable协议时, 才会添加isTop方法。

尝试在不符合equatable协议的元素上调用isTop方法, 会出现编译错误。

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

也可以在扩展协议时使用泛型where子句。

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

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

当然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"

上下文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"

如果不想在上下文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子句的关联类型

可以在关联类型中加入泛型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子句, 为关联类型添加约束。

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

容器协议的这一扩展增加了一个下标, 该下标接收一系列索引, 并返回一个数组, 其中包含每个给 定索引上的项目。限制如下:

  • 括号中的泛型参数 Indices 必须是符合 Swift 标准库中 Sequence 协议的类型。
  • 该下标的单个参数 indices 是 Indices 类型的实例。
  • 通用 where 子句要求序列的迭代器必须遍历 Int 类型的元素。

这些限制意味着为 indices 参数传递的值是一个整数序列。