美文网首页
Swift: 泛型

Swift: 泛型

作者: 伯wen | 来源:发表于2017-09-26 10:17 被阅读22次

    泛型的概念

    泛型代码可根据自定义需求,写出适用于任何类型、灵活且可重用的函数和类型,避免重复的代码,用一种清晰和抽象的思维表达代码的意思

    泛型函数

    示例:

    /// 交换变量的值
    func exchange<T>(_ one: inout T, _ two: inout T) {
        (one, two) = (two, one)
    }
    
    var a = 10.0
    var b = 20.0
    
    print("a = \(a), b = \(b)")     // a = 10.0, b = 20.0
    exchange(&a, &b)               // 交换a和b的值
    print("a = \(a), b = \(b)")     // a = 20.0, b = 10.0
    
    var c = "hellow"
    var d = "world"
    
    print("a = \(c), b = \(d)")     // a = hellow, b = world
    exchange(&c, &d)               // 交换c和d的值
    print("a = \(c), b = \(d)")     // a = world, b = hellow
    
    • 上述代码中, exchange(_:_:)函数就是一个泛型函数

    • <T>中的<T>是一个占位类型, 在定义过程中不确定具体的类型, 只有在函数调用时, 根据传入的值的类型, 来推断出T的具体类型

      • exchange(&a, &b)TDouble类型
      • exchange(&c, &d)TString类型

    注意: 泛型函数在调用的时候, 会根据传入的值推断出对应的类型

    • 泛型函数格式:
    func 函数名<占位类型列表>(参数列表) {
        // 函数体
    }
    
    • 注意:
      • 参数列表中, 占位类型列表中的占位类型必须在参数列表中使用
      • 如果参数列表中, 多个参数都属于相同的占位类型, 那么这些参数必需传入一致的类型数据, 例如: 全部传入Int, String, 或Double
      • 占位类型列表可以有多个占位类型, 使用逗号分开

    类型参数

    • exchange(_:_:)函数中, 占位类型T就是一个类型参数的例子, 即: T是一个类型参数
    • 类型参数指定并命名一个占位类型, 并且紧随在函数名后面, 使用一对尖括号括起来(例如: <T>)
    • 一旦一个类型参数被指定, 就可以如下使用:
      • 用来定义一个函数的参数类型(例如: exchange(_:_:)中的one和two的类型)
      • 做为函数的返回值
      • 函数主体中的注释类型
    • 类型参数的定义过程中不会代表任何具体的类型, 只是一个占位, 当函数被调用时, 会根据传入的值的类型, 推断出具体类型, 例如上面的DoubleStirng替换掉T
    • 参数类型可以同时存在多个, 并用逗号分开, 例如: <T, U, S>为三个类型参数(占位类型), 名称分别为参数类型T, 参数类型U, 参数类型S
    综上有泛型函数格式如下:
    func 函数名<类型参数列表>(参数列表) {
       // 函数体
    }
    

    泛型类型

    • 泛型函数是在函数名的后面紧跟着类型参数列表, 而泛型类型就是在定义的类型的时候, 在类型名后面紧跟类型参数列表
    示例
    • 泛型类:
    泛型类: 
    class GenericClass<Element> {
        // 集合
        var items = [Element]()
        // 压栈
        func push(_ item: Element) {
            items.append(item)
        }
        // 出栈
        func pop() -> Element? {
            return items.isEmpty ? nil : items.removeLast()
        }
    }
    
    • 泛型结构体
    泛型结构体:
    struct GenericStruct<Element> {
        // 集合
        var items = [Element]()
        // 压栈
        mutating func push(_ item: Element) {
            items.append(item)
        }
        // 出栈
        mutating func pop() -> Element? {
            return items.isEmpty ? nil : items.removeLast()
        }
    }
    
    • 泛型枚举:
    泛型枚举:
    enum GenericEnum<Element> {
        case none
        case some(Element)
    }
    
    • 上面的 GenericClass(类), GenericStruct(结构体), GenericEnum(枚举)都是泛型类型, 在类型名后紧跟着泛型的类型参数 <Element>
    • 泛型类型使用的时候, 需要指定类型参数的具体类型, 下面以结构体GenericStruct为例:
    // 创建GenericStruct类型的机构体变量struct
    // 指定类型参数为 Int
    var struct = GenericStruct<Int>()
    // 使用struct时, push(_:), pop()方法使用 类型参数的地方 都会替换为Int类型
    struct.push(1)
    struct.push(2)
    struct.push(3)
    struct.push("4")   // 报错: 因为push(_:)接收的参数类型已经被替换成Int
    
    let result = struct.pop()   // result = 3
    
    给泛型类型添加分类(extension)
    • 泛型类型添加分类时, 定义中不可以增加新的类型参数, 也不需要写已有类型参数(编译器也不允许写)
    • 错误写法:
    错误一: 分类的定义中不可以增加新的类型参数
    extension GenericClass<T> { }
    错误二: 分类的定义中不需要写已经有的类型参数
    extension GenericClass<Element> { }
    
    • 下面的代码是正确写法, 在分类中可以使用类型定义时有的类型参数:
    extension GenericClass {
        // 使用已有的 类型参数: Element 做为返回值
        func element(at index: Int) -> Element? {
            if index < items.count {
                return items[index]
            }else {
                return nil
            }
        }
    }
    

    虽然在分类中无法定义新的类型参数, 但是可以在分类新定义的方法中引入其他的类型参数

    extension GenericClass {
       func exchange<T>(one: inout T, two: inout T) {
           (one, two) = (two, one)
       }
    }
    

    泛型约束

    • 上述所有代码中, 不论是泛型函数中的 类型参数T , 还是泛型类型中的 类型参数Element , 都可以在调用时指定任意一个具体的类型做为替换, 这是因为我并没有给这些参数类型添加任何的约束
    那么什么是 参数类型添加约束呢?

    就拿我们经常使用的Dictionary为例, 我们知道Dictionary的定义中有两个参数类型, 分别为 KeyValue , 而且在给Dictionary添加元素的时候, key的值都是唯一的,即Dictionary根据Key的值来判断是修改还是增加元素, 而Swift中的Dictionary是根据Key的哈希值来判断唯一性的, 也就是说DictionaryKey值必须是可哈希的, 所以Dictionary的类型参数Key有一个约束, 那就是 可哈希的值

    • 下面是Dictionary定义的代码部分, 这里过滤里面的实现部分, 其中的where会在后面讲解
    public struct Dictionary<Key, Value> : Collection, ExpressibleByDictionaryLiteral where Key : Hashable{}
    
    类型约束语法
    • 在定义一个类型参数时, 在类型参数后面放置一个类名或者协议名, 并用冒号分开, 来定义类型参数类型约束, 他们将成为类型参数列表的一部分
    • 示例:
    泛型函数添加类型约束
    func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
        // 这里是反省函数的函数体部分
    }
    
    泛型类型添加类型约束
    class GenericClass<T: SomeClass, U: SomeProtocol> {
        // 类的实现部分
    }
    
    • 上面的 泛型函数泛型类型 都分别有两个类型参数 TU, T有一个类型约束: 必须是SomeClass类的子类; U有一个类型约束: 必须遵守SomeProtocol协议的类型
    类型约束实践
    • 现在有一个非泛型函数findIndex(ofString:in:), 该函数的功能是在一个String数组中查找给定的String值的索引, 若找到匹配的String值, 会返回该String值在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:)函数可以用于查找字符串数组中某个字符串的索引值
    let strings = ["a", "b", "c", "d", "e"]
    if let foundIndex = findIndex(of: "c", in: strings) {
        print("c 的索引值是 \(foundIndex)")
    }
    // 打印: c 的索引值是 2
    
    • 我们知道, findIndex(ofString:in:)函数目前只能查找字符串在数组中的索引值, 用处不是很大。不过, 我们可以用占位类型T替换掉String类型来写出具有相同功能的泛型函数findIndex(of:in:)
    • 下面就是使用占位类型T替换掉String类型的代码:
    func findIndex<T>(of valueToFind: T, in array: [T]) -> Int? {
        for (index, value) in array.enumerated() {
            if value == valueToFind {
                return index
            }
        }
        return nil
    }
    
    • 这段代码看上去可以查找任意的具体类型在该类型数组中的索引值, 但是在Xcode中并不能正常运行

    • 这是因为在Swift中, 想要比较两个值是否相等, 那么这两个值的类型必须实现了Equatable协议才可以

    • 对于未实现Equatable协议的类型, 比如我们自定义的类和结构体的实例, 是不能直接使用 == 来比较的, 因为这些实例并不知道"相等"意味着什么, 是部分内容相等才相等, 还是完全相等才算相等, 而Equatable就是用来说明"相等"意味着什么的

    • 因为只有遵守Equatable协议的类型才能进行相等判断, 所以上述可以被替换成为任意类型的T就不能符合要求, 所以我们需要给T加上一个类型约束: 想要替换占位类型T的具体类型, 必须遵守Equatable协议

    • 任何遵守Equatable协议的类型都可以在findIndex(of:in:)中正常运行, 代码如下:

    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

    关联类型(泛型协议)

    • 类、结构体和枚举的泛型类型中, 将类型参数列表放在了类型名的后面, 而在泛型协议中却不能这样写

    • 泛型协议的写法与泛型类型有所不同, 需要使用 associatedtype 关键字来指定类型参数

    • 泛型协议中的类型参数又被称为关联类型, 其代表的实际类型在协议被采纳时才会被指定

    • 下面一段代码就是有关联类型的协议:

    protocol Container {
        associatedtype ItemType
        mutating func append(item: ItemType)
        var count: Int { get }
        subscript(i: Int) -> ItemType { get }
    }
    
    • 协议Container定义了三个任何采纳该协议的类型必须提供的功能

      • 必须可以通过append(_:)方法添加一个类型为ItemType的新元素到容器里

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

      • 必须可以通过索引值类型为Int的下标检索到容器中的每一个类型为ItemType的元素

    • 协议中无法定义ItemType的具体类型, 而任何遵从Container协议的类型都必须指定关联类型 ItemType 的具体类型

    • 下面的是一个非泛型的IntStack结构体, 采纳并符合了Container协议, 实现了Container协议的是三个要求:

    struct IntStack: Container {
        // 集合数组, 用于存放元素
        var items = [Int]()
      
        // Container协议部分
        typealias ItemType = Int  // 通过关键字 typealias 指定ItemType的类型为Int
        mutating func append(item: Int) {
            self.push(item)
        }
        var count: Int {
            return items.count
        }
        subscript(i: Int) -> Int {
            return items[i]
        }
    }
    
    • IntStack在实现Container的要求时, 指定了ItemType的类型为Int, 即typealias ItemType = Int, 从而将Container协议中抽象的ItemType类型转换为具体的Int类型

    • 由于Swift的类型推断, 实际上不用再IntStack中特意的声明ItemType的类型为Int也可以, 这是因为IntStack符合Container协议的所有要求, 并且在方法中也将ItemType写成了Int类型, 这样Swift就可以推断出ItemType的类型为Int, 事实上, 在代码中删除typeealias IntType = Int这一行, 一切依旧可以正常工作

    struct IntStack: Container {
    
        var items = [Int]()
    
        mutating func append(item: Int) {
            self.push(item)
        }
        var count: Int {
            return items.count
        }
        subscript(i: Int) -> Int {
            return items[i]
        }
    }
    
    • 上面的代码自动推断出 IntType 的类型是 Int

    即: 对于泛型协议, 当有类型遵从该协议的时候, 只需要给未确定具体类型的关联类型所参与的所有方法中, 都给出唯一指定类型时, 并不需要特意声明该关联类型的声明也能正常运行, 原因就是Swift的自动推断

    • 也可以让泛型Stack结构体遵从Container协议
    struct Stack<Element>: Container {
        var items = [Element]()
        // 由于所有需要关联类型的地方都指定了明确类型, 就不需要在特意的声明关联类型具体是什么类型了, 这里自动推断出 ItemType的类型是Stack对象创建时指定的泛型具体类型
        // typealias ItemType = Element
        mutating func append(item: Element) {
            self.items.append(item)
        }
        var count: Int {
            return items.count
        }
        subscript(i: Int) -> Element {
            return items[i]
        }
    }
    
    通过扩展一个存在的类型来指定关联类型
    • 对于一个已经存在的类型, 并且在定义中没有遵守泛型协议, 我们可以在它的extension中遵守需要的泛型协议, 并且在该扩展中 也可以自动推导ItemType的类型, 并不需要写typealias ItemType = Element
    struct Stack<Element> {
        var items = [Element]()
    }
    // 通过扩展遵从泛型协议 Container
    extension Stack: Container {
        // 这一行可以不写
        // typealias ItemType = Element
    
        mutating func append(item: Element) {
            self.items.append(item)
        }
        var count: Int {
            return items.count
        }
        subscript(i: Int) -> Element {
            return items[i]
        }
    }
    

    泛型 Where 语句

    • 上面的叙述中, 类型约束让我们能够为泛型函数泛型类型类型参数定义一些强制要求
    • 除了类型约束以外, 还有一种方法给泛型函数泛型类型类型参数定义约束, 那就是where子句
    • 现有如下方法:
    func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
        for (index, value) in array.enumerated() {
            if value == valueToFind {
                return index
            }
        }
        return nil
    }
    
    • 该函数就是类型约束中使用的, 获取一个元素, 在该元素数组中的索引值的函数, 在占位类型T后 使用: EquatableT进行了类型约束。
    • 现在可以将该函数使用where子句变形为下面代码:
    func findIndex<T>(of valueToFind: T, in array: [T]) -> Int? where T : Equatable {
        for (index, value) in array.enumerated() {
            if value == valueToFind {
                return index
            }
        }
        return nil
    }
    
    • 变形后的代码, 将类型约束提取出来, 放在了返回值的后面, 使用where子句来表达该约束, 这在语法上没有任何问题, 并且变形后的函数依然可以正常使用

    • 当然, 如果仅仅是给类型参数添加类型约束, 仅仅需要第一种方式就可以了。 实际上where子句还有另外一个用法, 即: where子句除了给类型参数添加类型约束外, 还可以给关联类型添加约束

    • 通过下面的代码示例进行讲解where子句给关联类型添加约束的用法
    // 容器协议
    protocol Container {
        associatedtype ItemType
        
        mutating func append(item: ItemType)
    }
    
    func allItemsMatch<C1: Container, C2: Container>(_ someContainer: C1, _ anotherContainer: C2) -> Bool {
        // 检查两个容器包含相同数量的元素
        if someContainer.count != anotherContainer.count {
            return false
        }
        // 检查每个元素是否相等
        for i in 0..<someContainer.count {
            if someContainer[i] != anotherContainer[i] {
                return false
            }
        }
        return true
    }
    
    • 上述代码中, allItemsMatch(_:_:)函数是一个泛型函数, 作用是判断两个都遵守了Container协议的容器C1, C2中所有的元素是否在位置和值上完全相等
    • 由于C1和C2是两个不同的占位类型, 所以C1和C2可以是两个不同的类型
    // 遵守Container协议的类型Stack1
    class Stack1<Element>: Container {
        var items = [ItemType]()
        
        typealias ItemType = Element
        
        func append(item: Element) {
            items.append(item)
        }
    }
    
    // 遵守Container协议的类型Stack2
    class Stack2<Element>: Container {
        var items = [ItemType]()
        
        typealias ItemType = Element
        
        func append(item: Element) {
            items.append(item)
        }
    }
    
    • 上面定义了都遵守Container协议的两个类型Stack1Stack2, 我们将使用Stack1Stack2的实例对象进行比较

    • 根据allItemsMatch(_:_:)的作用可以判断出, 两个容器Stack1Stack2只有在元素类型(ItemType)类型一致的情况下才能判断元素是否相等, 但是这个约束使用类型约束中的方法无法添加, 所以就有了下面的写法:

    // 这里只考虑定义部分, 不考虑实现部分
    func allItemsMatch<C1: Container, C2: Container>(_ someContainer: C1, _ anotherContainer: C2) -> Bool where C1.ItemType == C2.ItemType {//函数体}
    
    • 这句代码中, 使用了where语句对C1C2的关联类型进行了约束, 即C1C2ItemType

    有了这个where子句后, 只有Stack1Stack2中的元素类型必须一致才能使用该函数

    • 当然仅仅有类型相等判断是不够的, 容器中的元素还必须遵守Equatable才行, 即C1.ItemType == C2.ItemType并且C1.ItemType: Equatable, 所以allItemsMatch(_:_:)函数的完整代码如下:
    func allItemsMatch<C1: Container, C2: Container>(_ someContainer: C1, _ anotherContainer: C2) -> Bool where C1.ItemType == C2.ItemType, C1.ItemType: Equatable {
        // 检查两个容器包含相同数量的元素
        if someContainer.count != anotherContainer.count {
            return false
        }
        // 检查每个元素是否相等
        for i in 0..<someContainer.count {
            if someContainer[i] != anotherContainer[i] {
                return false
            }
        }
        return true
    }
    
    • 在where子句中, 使用逗号分隔多个约束

    类型的定义中也可以使用where子句添加约束, 用法与在泛型函数中一样, 都写在定义的后面, 大括号{}的前面

    • 示例:
    class Stack<Element>: Container where Stack.ItemType : Equatable {
        var items = [ItemType]()
        
        typealias ItemType = Element
        
        func append(_ item: Element) {
            items.append(item)
        }
    }
    
    具有泛型where子句的扩展
    • 你可以使用泛型where子句做为扩展的一部分, 基于以前的例子, 下面的示例扩展了泛型Stack结构体, 添加一个isTop:方法
    extension Stack where Element: Equatable {
        func isTop(_ item: Element) -> Bool {
            return items.last == item
        }
    }
    
    • 以下是 isTop(_:) 方法的调用方式:
    if stackOfStrings.isTop("c") {
        print("Top element is c.")
    } else {
        print("Top element is something else.")
    }
    // 打印 "Top element is c."
    
    • 如果尝试在包含的元素不符合Equatable协议的栈上调用isTop(_:)方法, 则会收到编译时错误
    struct NotEquatable { }
    var notEquatableStack = Stack<NotEquatable>()
    let notEquatableValue = NotEquatable()
    notEquatableStack.push(notEquatableValue)
    notEquatableStack.isTop(notEquatableValue)  // 报错
    
    • 你可以使用where子句扩展一个协议, 基于以前的实例, 下面的实例扩展了Container协议, 添加一个startsWith(:_)方法
    extension Container where ItemType: Equatable {
        func startWith(_ item: ItemType) -> Bool {
            return count >= 1 && self[0] == item
        }
    }
    
    extension Array: Container{}
    
    let array = ["a", "b", "c"]
    
    if array.startWith("a") {
        print("array 第一个元素是 a")
    }else {
        print("array 第一个元素不是 a")
    }
    // 打印 array 第一个元素是 a
    
    • 除了给泛型类型泛型协议分类中添加上述的where子句外, 还可以直接约束ItemType的具体类型:
    extension Container where ItemType == Double {
        func average() -> Double {
            var sum = 0.0
            for i in 0..<count {
                sum += self[i]
            }
            return sum / Double(count)
        }
    }
    print([1260.0, 1200.0, 98.6, 37.0].average())
    // 打印 "648.9"
    
    • 只有容器中的元素为Double类型时, 才可以调用该方法
    具有泛型 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
    }
    
    • 上述代码Container协议中, 通过associatedtype Iterator定义了一个类型参数Iterator, 并使用类型约束, 使Iterator必须遵从IteratorProtocol协议, 又使用where子句, 约束了Iterator对应IteratorProtocol协议的类型参数的类型必须和associatedtype Item类型一致

    • 一个协议继承了另一个协议, 你通过在协议声明的时候, 包含泛型Where子句, 来添加一个约束到被继承协议的关联类型, 例如, 下面的代码声明了一个ComparableContainer协议, 他要求所有的Item必须是Comparable的

    protocol ComparableContainer: Container where Item: Comparable {}
    

    相关文章

      网友评论

          本文标题:Swift: 泛型

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