Swift 进阶 —— 泛型

| Swift , iOS

 

请注意,本文大部分内容摘自《Swift 进阶》

 

内容概览

  • 前言
  • 泛型类型
  • 扩展泛型类型
  • 泛型和 Any
  • 基于泛型的设计
  • 总结

 

前言

 

什么是泛型?《Swift 进阶》中给出的定义是:泛型编程是一种可以保持类型安全性的代码重用技术。 泛型也被成为 参数化多态

实际上,就是让编译器为结构相同的代码提供模板,在编译时为不同的数据结构生成对应的方法。在 C++ 中,多态的关键字就是 template

比如,我们要实现最大值函数用于获取两个整数、两个浮点数、两个字符串中较大的值。

如果不借助泛型,我们需要为每一种数据结构单独定义一个 max 函数:

func max(_ a: Int, _ b: Int) -> Int {
    return a > b ? a : b
}

func max(_ a: Float, _ b: Float) -> Float {
    return a > b ? a : b
}

func max(_ a: String, _ b: String) -> String {
    return a > b ? a : b
}

如果借助泛型,我们只需要定义一个 max 泛型函数,然后让每种数据结构遵循 Comparable 协议即可:

func max<T>(_ x: T, _ y: T) -> T where T : Comparable {
    return x > y ? x : y
}

泛型通常都会和协议成对出现,因为 Swift 要通过协议明确约束泛型参数的行为。上面的 max 泛型函数就是一个很好的例子,泛型 T 需要遵守 Comparable 协议。

Swift 中的泛型也非常强大,快来深入了解一下吧~

 

泛型类型

 

函数和方法并不是唯一的泛型类型。我们还可以有泛型结构体,泛型类和泛型枚举。

比如,为 Swift 可选链提供支持的 Optional 枚举类型就是基于泛型设计的:

enum Optional<Wrapped> {
    case none
    case some(Wrapped)
}

我们甚至可以基于泛型定义递归的数据结构:

enum BinaryTree<Element> {
    case leaf
    indirect case node(Element, l: BinaryTree<Element>, r: BinaryTree<Element>)
}

如果您打开 Xcode 中的文档查看 数组(Array) 在 Swift 中的定义,您会看到它是基于泛型设计的 struct

struct Array<Element>

泛型的应用几乎贯穿了整个 Swift 标准库的实现:

→ Optional 用泛型参数抽象它包装的类型。
→ Result 有两个泛型参数:分别表示成功和失败这两种结果对应的值的类型。
→ Unsafe[Mutable]Pointer 用泛型参数表示指针指向的对象的类型。
→ Key paths 中使用了泛型表示根类型以及路径的值类型。
→ 各种表示范围的类型,使用了泛型表达范围的上下边界。

 

扩展泛型类型

 

  • 添加泛型初始化方法:
extension BinaryTree { 
    init(_ value: Element) {
        self = .node(value, l: .leaf, r: .leaf)
    }
}
  • 添加泛型计算属性:
extension BinaryTree {
    var values: [Element] {
        switch self {
        case .leaf:
            return []
        case let .node(el, left, right): // 中序遍历
            return left.values + [el] + right.values
        }
    }
}

其实,和扩展普通类型比较类似,只是我们要用泛型类型替换某个具体的类型。

 

泛型和 Any

 

泛型和 Any 的用途是类似的,不过二者有以下不同:

  • Any 缺少了类型安全性,需要使用反射/内省或者类型转换才可以确定实例的具体类型;
  • 泛型在编译器确定类型,Any 在运行时确定类型,前者可以有效地提高运行时的性能;
  • 泛型代码更易读;

请观察以下示例代码:

extension Array {
    func reduce<Result>(_ initial: Result, _ combine: (Result, Element) -> Result) -> Result
}

从方法的签名中,我们便可大致理解其输入、输出和函数的主体功能。如果泛型遵循了协议,我们可以通过协议更好地理解参数的行为以及函数的作用效果。

作为对比,请观察以下 Any 版本的方法:

extension Array {
    func reduce(_ initial: Any, _ combine: (Any, Any) -> Any) -> Any
}

我们很难知道输入和输入的参数的性质,也很难理解这些 Any 类型的参数会怎样参与函数的处理过程。

 

从某种意义上说,一个函数或方法的泛型程度越高,它能做的事情就越少。

 

请观察 Swift Array的 append 方法的签名

func append<S>(contentsOf newElements: S) where S : Sequence, Element == S.Element

想要一次性地添加多个元素,这些元素需要被存储在一个序列中,且序列中的元素的类型与当前数组中元素的类型相同。

 

基于泛型的设计

 

我们将重构一个非泛型的网络代码,使用泛型提取出其中公共的功能。

请观察以下示例代码:

struct User: Decodable {}

enum NetworkError: Error {
    case noData
}

let webserviceURL = URL(string: "https://ficowshen.com")!

extension URLSession {
    func loadUser(callback: @escaping (Result<User, Error>) -> ()) {
        let userURL = webserviceURL.appendingPathComponent("/profile")
        dataTask(with: userURL) { data, response, error in
            callback(Result {
                if let e = error { throw e }
                guard let d = data else { throw NetworkError.noData }
                return try JSONDecoder().decode(User.self, from: d)
            })
        }.resume()
    }
}

我们可以把 User 替换成一个泛型参数,这段代码就不再依赖某个具体的 model 类型。

另外,还可以替换掉直接进行 JSON 解码的代码,给 load 添加另外一个参数: parse,让它负责解析 web service 返回的数据,并创建一个 A 类型的对象。

extension URLSession {
    func load<A>(url: URL,
                 parse: @escaping (Data) throws -> A,
                 callback: @escaping (Result<A, Error>) -> ()) {
        dataTask(with: url) { data, response, error in
            callback(Result {
                if let e = error { throw e }
                guard let d = data else { throw NetworkError.noData }
                return try parse(d)
            })
        }.resume()
    }
}

接下来,我们就可以用这个泛型方法加载不同的请求了。

加载 User

let profileURL = webserviceURL.appendingPathComponent("profile")

URLSession.shared.load(url: profileURL, parse: {
    try JSONDecoder().decode(User.self, from: $0)
}) { print($0) }

加载 BlogPost

struct BlogPost: Decodable {}

let postURL = webserviceURL.appendingPathComponent("blog")

URLSession.shared.load(url: postURL, parse: {
    try JSONDecoder().decode(BlogPost.self, from: $0)
}) { print($0) }

由于 URL 和进行数据解码的 parse 函数总是成对出现的,一个更好的做法是把它们封装到一个 Resource 结构体中:

struct Resource<A> {
    let url: URL
    let parse: (Data) throws -> A
}

let profile = Resource<User>(url: profileURL, parse: {
    try JSONDecoder().decode(User.self, from: $0)
})

let post = Resource<BlogPost>(url: postURL, parse: {
    try JSONDecoder().decode(BlogPost.self, from: $0)
})

为了避免 JSONDecoder 代码的重复,可以给 Resource 添加一个便利初始化方法:

extension Resource where A: Decodable {
    init(json url: URL) {
        self.url = url
        self.parse = { data in
            try JSONDecoder().decode(A.self, from: data)
        }
    }
}

此时,就可以更精炼的定义表示 JSON 的资源:

let profile = Resource<User>(json: profileURL)
let blogPost = Resource<BlogPost>(json: postURL)

最后,为 URLSession 增加一个支持 Resource 的泛型方法:

extension URLSession {
    func load<A>(_ r: Resource<A>,
                 callback: @escaping (Result<A, Error>) -> ()) {
        dataTask(with: r.url) { data, response, err in
            callback(Result {
                if let e = err { throw e }
                guard let d = data else { throw NetworkError.noData }
                return try r.parse(d)
            })
        }.resume()
    }
}

这样,就可以便捷地获取用户的信息了:

URLSession.shared.load(profile) { result in
    print(result)
}

我们一般会基于泛型来构建通用数据结构,因为数据结构是完全抽象的,可以完全不依赖于具体内容的类型。

比如,Swift 标准库中不包含的队列:

struct Queue<Element> {
    private var enqueueStack = [Element]()
    private var dequeueStack = [Element]()

    var first: Element? { dequeueStack.last }

    var isEmpty: Bool { dequeueStack.isEmpty && enqueueStack.isEmpty }

    var count: Int { dequeueStack.count + enqueueStack.count }

    mutating func enqueue(_ element: Element) {
        enqueueStack.append(element)
    }

    mutating func enqueue(contentsOf elements: [Element]) {
        enqueueStack.append(contentsOf: elements)
    }

    mutating func dequeue() -> Element? {
        if dequeueStack.isEmpty {
            dequeueStack = enqueueStack.reversed()
            enqueueStack.removeAll()
        }
        return dequeueStack.popLast()
    }
}

以及优先队列(基于二叉堆实现):

struct PriorityQueue<T> {

    var isEmpty: Bool { heapSize == 0 }

    /// Returns the max value. To return min value, insert with the opposite numbers.
    var top: T {
        if isEmpty { fatalError() }
        return heap[0]
    }

    var size: Int { heapSize }

    private let degree = 2
    private let sort: ((T, T) -> Bool)
    private var heap = [T](), heapSize = 0

    private var isFull: Bool { heapSize == heap.count }

    init(capacity: Int, defaultValue: T, sort: @escaping ((T, T) -> Bool)) {
        self.sort = sort
        heap = [T](repeating: defaultValue, count: capacity)
    }

    /// O(log N)
    mutating func insert(_ x: T) {
        if isFull { assertionFailure("heap is full") }
        heap[heapSize] = x
        heapSize += 1
        heapifyUp(heapSize - 1)
    }

    /// O(log N)
    @discardableResult
    mutating func pop() -> T { deleteAt(0) }

    /// O(log N)
    @discardableResult
    mutating func deleteAt(_ index: Int) -> T {
        if isEmpty { assertionFailure("heap is empty") }
        let element = heap[index]
        heap[index] = heap[heapSize - 1]
        heapSize -= 1
        heapifyDown(index)
        return element
    }

    /// Adjusts the last number, move it up if it's bigger than its children.
    /// - Parameter index: The end index of the heap
    private mutating func heapifyUp(_ index: Int) {
        var i = index
        let insertValue = heap[i]
        while i > 0, sort(insertValue, heap[parent(i)]) {
            heap[i] = heap[parent(i)]
            i = parent(i)
        }
        heap[i] = insertValue
    }

    /// Adjusts the first number, move it down if it's smaller than its children.
    /// - Parameter index: The start index of the heap
    private mutating func heapifyDown(_ index: Int) {
        var i = index, child = 0, temp = heap[i]
        while kthChild(i, 1) < heapSize {
            child = nextChild(i)
            if sort(temp, heap[child]) { break }
            heap[i] = heap[child]
            i = child
        }
        heap[i] = temp
    }

    private func parent(_ i: Int) -> Int {
        return (i - 1) / degree
    }

    private func kthChild(_ i: Int, _ k: Int) -> Int {
        return degree * i + k
    }

    private func nextChild(_ i: Int) -> Int {
        let leftChild = kthChild(i, 1)
        let rightChild = kthChild(i, 2)
        return sort(heap[leftChild], heap[rightChild]) ? leftChild : rightChild
    }
}

看官,您是否感觉到了泛型的强大了呢?

 

总结

 

对于与类型无关的代码,我们可以用泛型将其抽象出来,并以此划分出更加清晰的责任边界。

响应式编程中的各种 operators 就是最好的例子,比如:map, flatMap, reduce 等等。

善用泛型,我们可以就有效地避免重复造轮子,最终提高代码的质量和减少项目维护的工作量。

 

阅读本书使我获益良多,衷心地感谢优秀的内容生产者们!

如需深入学习相关内容,推荐大家购买原版书籍 《Swift 进阶》
请大家支持原创,用行动去鼓励高质量的内容生产者!

 

觉得不错?点个赞呗~

本文链接:Swift 进阶 —— 泛型

转载声明:本站文章如无特别说明,皆为原创。转载请注明:Ficow Shen's Blog

评论区(期待你的留言)