美文网首页
Swift 范型(Generics)详解

Swift 范型(Generics)详解

作者: WSJay | 来源:发表于2018-08-27 11:26 被阅读0次

    一、泛型解决的问题

    首先来看一个实际开发中经常遇到的简单问题,这是一个标准的非泛型函数swapTwoInts(::),它可以交换两个Int值:

    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"
    

    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(_:_:)三个函数体的功能是相同的。唯一区别是它们接收的参数类型分别不同。我们可以用泛型函数来解决这种因参数类型不同,而处理逻辑相同而导致编写大量重复代码的问题。

    二、泛型函数

    泛型函数可以使用任何类型。这是上面函数swapTwoInts(_:_:)的通用版本,称为swapTwoValues(_:_:)

    func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
        let temporaryA = a
        a = b
        b = temporaryA
    }
    
    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的占位符类型名,而不是实际的类型名。占位符类型名没有指明T必须是哪种类型,只是指明了参数ab必须是同一类型T。因为T是占位符,所以Swift不会查找T的实际类型。

    三、类型参数

    • 类型参数指定并命名占位符类型,将其写在函数名之后的一对尖括号(<>)内。
    • 通过在尖括号(用逗号分隔)中写入多个类型参数名称,可以提供多个类型参数。
    • 在大多数情况下,类型参数具有描述性名称,例如Dictionary中的<Key, Value>Array中的<Element>,这说明了类型参数和它所使用的泛型类型或函数之间的关系。若它们之间没有任何有意义的关系,则通常使用单个字母命名,如T、U和V。

    四、泛型类型

    除泛型函数,Swift还允许自定义泛型类型,这些可以是自定义类,结构和枚举。
    首先来看一个非泛型的堆栈示例:

    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()
        }
    }
    
    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."
    

    六、类型约束

    有时需要对泛型函数和泛型类型中使用的类型添加某些约束,使得只有满足该约束的类型才可以在泛型函数和泛型类型中使用。类型约束指定类型参数必须从特定的类继承,或符合特定的协议或协议组合。

    (一)类型约束的语法

    下面显示了泛型函数的类型约束的基本语法:

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

    上面的假设函数有两个类型参数。第一个类型参数的约束是: T必须是SomeClass的子类。第二个类型参数的约束是,U必须符合协议SomeProtocol

    (二)类型约束的应用

    首先来看一个示例,非泛型函数findIndex(ofString:in:)的功能是在字符串数组array中查找指定元素的索引值。

    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 value == valueToFind。在Swift语言中,不是每种数据类型都可以使用等号操作符(==)进行比较。只用遵守并实现了Equatable协议的类型才可以使用==操作符。Swift中的所有标准类型都自动支持Equatable协议。
    因此上面的函数的泛型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
    

    七、关联类型

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

    (一)关联类型应用

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

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

    下面的非泛型类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]
        }
    }
    

    还可以使泛型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可以推断出ElementItem用作这个特定容器的合适类型。

    (二)扩展已有类型
    • 我们不仅可以通过扩展,使现有类型遵守一个协议,还可以使其遵守一个关联类型的协议。
      Swift的数组类型已经提供了一个append(_:)方法、一个count属性和一个带有Int索引的下标来检索其元素。这三个功能符合Container协议的要求。这意味着可以通过声明Array采用协议来扩展Array以符合Container协议。可以使用一个空扩展来完成此操作,正如在声明采用带有扩展的协议时所述:
    extension Array: Container {}
    
    (三)向关联类型添加约束
    • 可以向协议中的关联类型添加类型约束,以要求符合这些约束的类型满足这些约束。
      下例中,Container协议要求其中的元素类型要满足Equatable协议:
    protocol Container {
        associatedtype Item: Equatable
        mutating func append(_ item: Item)
        var count: Int { get }
        subscript(i: Int) -> Item { get }
    }
    
    (四)在关联类型的约束中使用协议

    协议可以作为其自身需求的一部分出现。例如,这里有一个改进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语句。
      下面是通过扩展,使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
        }
        // 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
    

    在上面示例中,StackSuffix关联类型也是Stack,因此Stack上的suffix操作返回另一个Stack。另外,遵守SuffixableContainer协议的类型可以具有与其本身不同的Suffix关联类型——即方法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
        }
        // Inferred that Suffix is Stack<Int>.
    }
    

    八、泛型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."
    

    在上例中,要求函数的两个类型参数需要满足以下约束条件:

    • C1必须符合Container协议(即C1: Container)。
    • C2还必须符合Container协议(即C2: Container)。
    • C1的Item必须与C2的Item相同(即 C1.Item == C2.Item)。
    • C1的Item必须符合Equatable协议(即C1.Item: Equatable)。
      而函数的两个参数someContaineranotherContainer则需要满足以下约束:
    • someContainer是类型C1的容器。
    • anotherContainer是C2类型的容器。
    • someContaineranotherContainer中的元素类型相同。
    • someContainer中的项可以使用不等运算符(!=)来检查它们是否不同。

    九、泛型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
        }
    }
    
    if stackOfStrings.isTop("tres") {
        print("Top element is tres.")
    } else {
        print("Top element is something else.")
    }
    // Prints "Top element is tres."
    

    上例通过使用泛型where分句向扩展添加新的约束条件,只有当添加到栈中的item符合Equatable协议才添加isTop(_:)方法。若向isTop(_:)方法中传入的参数不符合Equatable协议,则无法通过编译:

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

    十、泛型where分句的关联类型

    可以在关联类型上包含泛型where分句。例如,创建一个包含迭代器的Container实例:

    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分句,可以向继承的关联类型添加约束。例如,下面的代码声明了一个 ComparableContainer协议,它要求项目符合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
        }
    }
    

    十二、其他专题模块

    Swift 4.2 基础专题详解

    相关文章

      网友评论

          本文标题:Swift 范型(Generics)详解

          本文链接:https://www.haomeiwen.com/subject/vpnubftx.html