【iOS】objc.io – Swift 进阶 – 整理 – (八)范型

泛型编程是一种可以保持类型安全性的代码重用技术。如,标准库通过泛型编程让 sort 方法可以接受一个自定义的比较操作符,并且,让这个比较操作符的参数和进行排序的序列中元素的类型相同。类似的,为了以类型安全的方式提供访问和修改数组的API,Array也是一个泛型类型。

通常指的是对类型泛化后的编程 (一个显著的语法特征就是要使用尖括号具像化这些泛化的类型,例如:Array<Int>)。不过,泛型这个概念却远不止泛化 类型。我们可以认为泛型是多态(polymoyphism)的一种形式,而多态则是指一个接口或名称可以通过多个类型进行访问的现象。

至少有四种不同的概念,可以归纳到多态编程这个范畴里:

  • 可以定义多个同名但是类型不同的方法。例如,定义了三个不同的 sort,它们每个都有不同的参数类型。这种用法叫做重载 (overloading),或者更技术地说,这是一种 (为了解决排序这个问题而特别设置的) 专属多态 (ad hod polymorphism)

  • 当一个函数或方法接受类 C 作为参数的时候,我们也可以给它传递 C 的派生类,这种用法叫做子类型多态 (subtype polymorphism)

  • 当一个函数 (通过尖括号语法) 接受泛型参数的时候,我们管这个函数叫做泛型函数 (generic function),类似地,还有泛型类型和泛型方法。这种用法叫做参数化多态 (parametric polymorphism)。这些泛型化的参数,叫做泛型 (generics)

  • 可以定义一个协议并让多个类型实现它。这是另外一种更加结构化的专属多态

主要讨论第三种技术,也就是参数化的多态。

泛型类型

编写的一个最普通的函数,就是恒等函数 (identify function)。例如,一个原封不动返回参数的函数:

func identity<A>(_ value: A) -> A { 
    return value
}
复制代码

这个恒等函数有一个泛型类型 (generic type):对于任何类型 A,这个函数的类型就是 (A) -> A。 但是,这个函数却有无数多个具体类型 (concrete type),也就是不带泛型参数的类型。

函数和方法并不是唯一的泛型类型。还可以有泛型结构体,泛型类和泛型枚举。例如,下面就是 Optional 的定义:

enum Optional<Wrapped> { 
    case none
    case some(Wrapped)
}
复制代码

Optional 是一个泛型类型。为 Wrapped 选择了一个值,就得到了一个具体类型, 例如:Optional<Int>Optional<UIView>。可以把 Optional 当作一个类型构建器 (type constructor):给它传递一个具体类型 (例如:Int),它就会创建一个新的具体类型 (例如: Optional<Int>)。

浏览 Swift 标准库,就会看到其中包含了很多具体类型,但也有很多泛型类型 (例如:Array,Dictionary 和 Result)。Array 有一个泛型参数 Element,这就让我们可以使用任何一个具体类型来创建数组。还可以创建自己的泛型类型。例如,这是一个描述二叉树的枚举:

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

BinaryTree 是个单泛型参数的泛型类型。为了创建一个具体类型,必须为 Element 设置一个具体类型,例如 Int:

let tree: BinaryTree<Int> = .node(5, l: .leaf, r: .leaf)
复制代码

把泛型类型转换成具体类型的时候,一个泛型参数只能对应一个具体类型。例如,当创建空数组的时候,我们必须提供明确的数组类型,否则 Swift 编译器会抱怨说无法确认数组元素的具体类型:

var emptyArray: [String] = []
复制代码

类似地,除非明确指定 Array 中的 Element 可以用不同类型的值表示,否则 Swift 不允许在数组中存放不同类型的值。

let multipleTypes: [Any] = [1, "foo", true]
复制代码

扩展泛型类型

在 BinaryTree 的作用域里,泛型参数 Element 都是可用的。例如,当为 BinaryTree 编写扩展 的时候,也可以像使用一个具体类型一样来使用 Element。可以给 BinaryTree 添加一个使用 Element 作为参数的便利初始化方法 (convenience initializer):

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

// 访问 BinaryTree<Int> 中的 values 时,得到的就是一个整数数组:

tree.values // [5]
复制代码

还可以定义泛型方法。例如,给 BinaryTree 添加一个 map 方法。这个方法有一个额外的泛型参数 T,表示转换方法的返回值,也就是新的 BinaryTree 中节点的值的类型。由于这个方法也定义在 BinaryTree 的扩展里,仍旧可以使用 Element:

extension BinaryTree {
    func map<T>(_ transform: (Element) -> T) -> BinaryTree<T> {
        switch self { 
        case .leaf:
            return .leaf
        case let .node(el, left, right):
            return .node(transform(el), 
                         l: left.map(transform),
                         r: right.map(transform))
        } 
    }
}
复制代码

由于 Element 和 T 都没有协议约束,这里可以使用任意类型。甚至让这两个泛型参数是同一个具体类型也没问题:

let incremented: BinaryTree<Int> = tree.map { $0 + 1 }
// node(6, l: BinaryTree<Swift.Int>.leaf, r: BinaryTree<Swift.Int>.leaf)
复制代码

是不同的类型也可以。在下面这个例子中,Element 是 Int,T 是 String:

let stringValues: [String] = tree.map { "\($0)" }.values // ["5"]
复制代码

在 Swift 里,很多集合类型都是泛型类型 (例如:Array,Set 和 Dictionary)。但是,泛型这项技术本身可不仅仅用在表达集合类型上。它的应用几乎贯穿了整个 Swift 标准库的实现,例如:

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

泛型和 Any

通常,泛型和 Any 的用途是类似的,但它们有截然不同的表现。在没有泛型的编程语言里,Any 通常用来实现和泛型同样的效果,但是却缺少了类型安全性。这通常意味着要使用一些运行时特性,例如内省 (introspection) 或动态类型转换,把 Any 这种不确定的类型变成一个确定的具体类型。而泛型不仅能解决绝大部分同样的问题,还能带来编译期类型检查以及提高运行时性能等额外的好处。

阅读代码的时候,泛型可以帮助理解一个函数或方法执行的任务。例如,对于下面这个处理数组的 reduce 方法:

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

无需查看它的实现,从它的签名中就能大致描述出它的功能:

  • 首先, Result 是 reduce 的泛型参数,也是它的返回值类型 (这里 Result 只是泛型参数的名称)。
  • 其次,来看参数。reduce 接受一个 Result 类型的值,以及一个把 Result 和 Element 合成新的 Result 值的方法作为参数。
  • 由于返回值的类型是 Result,reduce 的返回值只能是 initial 或者调用 combine 得到的结果。
  • 如果数组是空的,就没有用于合成的 Element,这时能返回的只有 initial。
  • 如果数组不为空,reduce 的类型就给它的实现提供了自由:它即可直接返回 initial 而根本不去合并数组中的元素,也可以只合并数组中的特定元素 (例如,只合并第一个或最后一个元素),或逐个合并数组的每一个元素。

reduce 是可以有无数种实现方式的。例如,只针对数组中的某些特定元素调用 combine。它还可以使用一些运行时类型自省特性,修改一些全局状态,或者发起某些网络请求。由于标准库已经实现了 reduce 方法,能从它的实现确认什么才属于合理实现的范畴。

现在,我们再来看 Any 版本的 reduce:

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

即使我们假设 reduce 有一个合理的实现,单从签名看,这个声明传递出来的类型信息还是太少了。根本无法从这个签名中了解到第一个参数和方法返回值的关系,也无法从 combine 中了解到它的两个参数究竟是如何进行合并的。其实,甚至都不知道 combine 合并的是上一次的累计结果和数组中的下一个元素。

在经验里,泛型类型对于源码阅读有很大帮助。更确切地说,只要看到形如 reduce 或 map 这样的函数或方法,我们不用去猜它们的功能,单就签名中的泛型类型,就已经约束了可能的实现方法。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享