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,这样能够保障两个参数在交换后对于后续的处理在类型上是安全的。