1.2 泛型

泛型的概念最早出自C++的模板,Swift的泛型和C++模板设计的思路是一致的。为什么不与Java的泛型一致呢?原因是C++是一种编译时多态技术,而Java是运行时多态技术。Java运行时多态是在运行时才能确定的,所以会有运行时额外的计算,缺点是通过类型擦除生成的代码只有一份。而C++在编译时通过编译器来确定类型,所以在运行时就不需要额外计算了,这样效率会高一些,缺点是生成的机器码的二进制包会大一些,虽然执行快但可能会有更多的I/O。Swift采用编译时多态技术一方面是和C++一样在I/O性能和执行效率中选择了执行效率,另一方面是为了让代码更加安全。当Java在类型擦除中进行向下转型时丢失的类型只有在运行时才能看到。而Swift提供了额外的类型约束,使得泛型在定义的时候就能够判定类型。

使用泛型编写代码可以让我们写出更简洁、安全的代码。类型转换的代码也可以由编译器完成,减少了手写类型强制转换带来的运行时出错的问题。随着编译器或虚拟机技术的不断进步,在代码不变的情况下,优化工作会在编译层面完成,从而获得潜在性能方面的收益。综上所述,泛型可以写出可重用、支持任意类型的函数,以及灵活、清晰、优雅的代码。

下面我们用示例分析泛型是如何解决具体问题的。

let dragons = ["red dragon", "white dragon", "blue dragon"]
func showDragons(dragons : [String]) {
    for dragon in dragons {
        print("\(dragon)")
    }
}
showDragons(dragons: dragons)

如果用数字类型作为编号来代表每只龙,并且需要一个函数来显示出它们的数字编号,那么需要再写一个函数,如下所示。

let dragons = [1276, 8737, 1173]
func showDragons(dragons : [Int]) {
    for dragon in dragons {
        print("\(dragon)")
    }
}
showDragons(dragons: dragons)

以上两个函数里的内容基本一样,只是参数的类型不同,此时就是使用泛型的最佳时刻。在Swift中,泛型能够在function、class、struct、enums、protocol和extension中使用。首先看一下function是怎么使用泛型的,如下所示。

let dragonsId = [1276, 8737, 1173]
let dragonsName = ["red dragon", "blue dragon", "black dragon"]
func showDragons<T>(dragons : [T]) {
    for dragon in dragons {
        print("\(dragon)")
    }
}
showDragons(dragons: dragonsName)
showDragons(dragons: dragonsId)

上面的类型参数T最好能够具有一定的描述性,就像字典(Dictionary)定义中的key和value,以及数组(Array)里的element一样具有描述性。Swift的基础库大量使用泛型,例如数组和字典都是泛型集合。我们既可以创建Swift支持的任意类型的数组,也可以创建存储任意类型值的字典。下面我们看一看系统提供的swap方法是如何运用泛型的,代码在Swift的源代码路径stdlib/public/core/MutableCollection.swift里,如下所示。

// the legacy swap free function
//
/// Exchanges the values of the two arguments.
///
/// The two arguments must not alias each other. To swap two elements of 
/// a mutable collection, use the 'swapAt(_:_:)' method of that collection
/// instead of this function.
///
/// - Parameters:
///   - a: The first value to swap.
///   - b: The second value to swap.
@inlinable
public func swap<T>(_ a: inout T, _ b: inout T) {
  // Semantically equivalent to (a,b) = (b,a).
  // Microoptimized to avoid retain/release traffic.
  let p1 = Builtin.addressof(&a)
  let p2 = Builtin.addressof(&b)
  _debugPrecondition(
    p1 != p2,
    "swapping a location with itself is not supported")
     
  // Take from P1.
  let tmp: T = Builtin.take(p1)
  // Transfer P2 into P1.
  Builtin.initialize(Builtin.take(p2) as T, p1)
  // Initialize P2.
  Builtin.initialize(tmp, p2)
}

这里的两个参数a和b使用了同样的泛型T,这样能够保障两个参数在交换后对于后续的处理在类型上是安全的。