本文源自于泊学文档,同时在下方添加了扩展的案例,为了方便团队成员翻阅,记录之。
面向对象的方式
在面向对象的世界里,经常会发生下面这样的事情。假设我们有一个驾驶汽车的函数,它接受一个Car
类型的参数,通过查看Car
的文档我们知道,它有selfCheck
/ startEngine
/ shiftUp
/ go
这4个方法:
func drive(_ car: Car) {
if !car.selfCheck() {
car.startEngine()
car.shiftUp()
car.go()
}
}
但是通常,一个类支持的方法要比我们在某个具体的业务逻辑中使用的丰富:
class Car {
func selfCheck() -> Bool {
return true
}
func startEngine() {}
func shiftUp() {}
func go() {}
func lightUp() {}
func horn() {}
func lock() {}
// ...
}
因此,对于drive
这个算法来说,虽然它只要求它的参数支持“自检”、“启动引擎”、“升挡”以及“前进”就好了。但是由于在声明里,参数的类型被定义成了Car
,因此,无论需要与否,它必须是一个完整的Car
类型。所谓完整,就是严格按照Car
的规格包含所有的init
,deinit
以及Car
的所有方法和属性。
因此,即便一个类型实现了drive
中的所有方法,但是只要它不在Car
的继承体系里,drive
就无法正常工作。我们管这种面向对象的方式约定的接口,叫做explicit interface
。
并且,由于Car
是一个class
,当我们传递Car的不同派生类时,各种方法的调用会在运行时被动态派发,这就是我们熟悉的运行时多态。
泛型编程的思维
但在泛型编程的世界里,故事就不同了。我们把之前的drive
方法先改成这样:
func drive<T>(_ car: T) {
if !car.selfCheck() {
car.startEngine()
car.shiftUp()
car.go()
}
}
从字面上看,意思是说,drive
接受一个T
类型的参数,只要这个类型支持了“自检”、“启动引擎”、“升挡”以及“前进”这4个操作,就可以把车开走。当然,和C++
中的泛型编程不同,在语法上,Swift
要求我们明确把刚才这个要求表达出来,而不能仅仅通过drive
的实现隐式表达这个要求。而这,就是protocol
的作用:
泛型与Protocol
protocol Drivable {
func selfCheck() -> Bool
func startEngine()
func shiftUp()
func go()
}
然后,在drive
的实现里,我们也要明确的指定这个要求:
func drive<T: Drivable>(_ car: T) {
if !car.selfCheck() {
car.startEngine()
car.shiftUp()
car.go()
}
}
现在,对于drive
这个算法来说,它的要求就比之前面向对象的版本精确多了。它唯一的要求,就是类型支持算法需要的4个方法就好了,至于这个类型的对象如何初始化,如何被回收,有什么属性,统统没有约定。因此,我们管通过泛型方式约定的接口,叫做implicit interface
。
然后,假设现在我们有两个独立的class
,表示两类不同的汽车:
class Roadster: Drivable {
func selfCheck() -> Bool {
return true
}
func startEngine() {}
func shiftUp() {}
func go() {}
}
class SUV: Drivable {
func selfCheck() -> Bool {
return true
}
func startEngine() {}
func shiftUp() {}
func go() {}
}
当我们分别对它们的对象调用drive时:
drive(Roadster())
drive(SUV())
结构体中的泛型
//MARK: - 结构体中的泛型
struct Point <T> {
var x : T
var y : T
}
当我们调用Point时:
let point1 = Point(x: 1 , y: 2)
print("\(point1.x), \(point1.y)")
类中的泛型
class Stack <T>{
var array: [T] = []
func pop() -> T? {
return array.removeLast()
}
func push(_ value : T) {
array.append(value)
}
}
调用结果如下:
let stack = Stack<Int>()
stack.push(2)
stack.push(3)
print(stack.pop()!)
print(stack.pop()!)
和面向对象中的运行时多态不同,泛型编程中调用方法的选择是在编译期完成的。编译器会根据参数的类型在正确的类中选择要调用的方法。这种行为,叫做编译期多态。
具象函数和泛型函数的解析顺序
全局函数的匹配顺序
假设,我们有一个计算余数的方法家族mod,它有两个接受Double和Float的具象版本,以及一个接受整数的泛型版本:
func mod(_ m: Double, by n: Double) -> Double {
print("Double ver.")
return m.truncatingRemainder(dividingBy: n)
}
func mod(_ m: Float, by n: Float) -> Float {
print("Float ver.")
return m.truncatingRemainder(dividingBy: n)
}
func mod<T: Integer>(_ m: T, by n: T) -> T {
print("Generic ver.")
return m % n
}
然后分别用下面的方法来测试:
mod(8, by: 2.5) // Double ver.
let eight: Float = 8
mod(eight, by: 2.5) // Float ver.
mod(8, by: 2) // Generic ver.
从注释中的结果可以看到,编译器会根据参数的类型自动为我们选择要调用的函数。但就像我们在上一节中提到的一样,这个过程是在编译期完成的。我们来看下面这个例子:
let doubles = [2, 3, 4.5]
doubles.forEach { _ = mod($0, by: 2) }
// Double ver.
// Double ver.
// Double ver.
这次,尽管我们的double
数组中同时包含了整数和浮点数,但编译器并不会生成在运行时根据读取的数值类型不同,调用不同mod
方法的代码。它只会为我们统一静态绑定到type inference
推断出来的Double
版本上。