泛型编程是一种可以保持类型安全性的代码重用技术。如,标准库通过泛型编程让 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 这样的函数或方法,我们不用去猜它们的功能,单就签名中的泛型类型,就已经约束了可能的实现方法。