软件设计

OOP多态完整解析:从重写到协议一次掌握

想一次理解 OOP 多态,从重写到协议,却越看越糊涂吗?

3 分钟阅读
OOP多态完整解析:从重写到协议一次掌握 封面图

想一次理解 OOP 多态,从重写到协议,却越看越糊涂吗?

重载、重写和协议看起来很相似,很容易混淆。

先说结论:多态就是“同名调用会根据目标产生不同的行为”。重写通过继承实现,而协议通过约定实现。

本文会逐步讲清楚这三个概念之间是如何关联的。

多态到底是什么?

多态(Polymorphism)在希腊语中意为“多种(poly)形态(morph)”。

简单来说,即使发送相同的消息,不同对象也会做出不同响应。

例如,同样让狗和猫“发出声音”,狗会汪汪叫,猫会喵喵叫。

调用方只需要调用소리내()。具体产生什么内容,由每个对象自行决定。

多态的核心是“调用方不需要知道”。

无论传入什么,都可以用同一份代码处理。

这让代码更加灵活。即使添加新动物,也不需要修改调用代码。


重载和重写有什么区别?

它们的名称相似,这是最容易混淆的地方。

重载是指创建多个同名函数,只改变参数。

重写是指子类重新定义并覆盖父类创建的函数。

只听文字不容易理解,我们直接看代码。

class Animal {
    func sound() { print("...") }
}

class Dog: Animal {
    override func sound() { print("汪汪") }  // 重写
}

let a: Animal = Dog()
a.sound()
// 输出:汪汪

有趣的是,虽然a的类型是Animal,却会发出Dog的声音。

这就是在运行时查看实际对象后决定的多态。

一个父类下,子类分别以不同方式重写行为
一个父类下,子类分别以不同方式重写行为

重载则有所不同。要调用哪个函数,会在编译时确定。

func add(_ a: Int, _ b: Int) -> Int { a + b }
func add(_ a: String, _ b: String) -> String { a + b }

print(add(1, 2))       // 输出: 3
print(add("甲", "乙"))  // 输出:甲乙

因此,重载也叫静态多态,重写则叫动态多态。


为什么需要协议?

只靠继承也能实现多态,那为什么还需要协议?

继承表示“is-a”关系,因此只能有一个父类。猫很难既是动物,又同时属于另一个层级。

协议采用“只要遵守这份约定,什么类型都可以”的方式,因此自由得多。

protocol Soundable {
    func sound()
}

struct Cat: Soundable {
    func sound() { print("喵喵") }
}

struct Car: Soundable {
    func sound() { print("嘀嘀") }
}

let things: [Soundable] = [Cat(), Car()]
things.forEach { $0.sound() }
// 输出:喵喵
// 输出:嘀嘀

猫和汽车没有任何继承关系,但仍然可以放在同一个数组中,以相同方式处理。

不依赖继承关系也能获得多态,这就是协议的力量。

这也是 Swift 等语言优先使用协议而不是继承的原因。

亲自敲代码后,我掌握了 protocol 和 override 的感觉
亲自敲代码后,我掌握了 protocol 和 override 的感觉

什么时候使用继承,什么时候使用协议

我把实际工作中的判断标准整理成了表格。

情况 判断
想复用公共代码 继承(重写)
没有关系,但想保证相同的行为 协议
想同时赋予多种特性 协议(可以同时采用多个)
想让值类型(struct)具备多态 协议

只要记住几个简单原则即可。

  • 需要复用大量父类实现时,使用继承
  • 想定义“能够做什么”时,使用协议
  • 如果关注可扩展性,优先考虑协议

面试时会这样问

Q. 请说明重载和重写的区别。

重载是在同一个名称下定义不同参数,在编译时确定。重写是子类重新定义继承的方法,在运行时根据实际对象确定。因此,前者称为静态多态,后者称为动态多态。

Q. 使用协议代替继承有什么好处?

继承只能有一个父类,不能用于值类型;协议则可以同时采用多个,也可以应用于 struct。没有关系的类型也能通过同一约定归为一组并统一处理,因此更有利于扩展。


多态起初可能很抽象,但只要抓住“调用方不需要知道”这句话,其他内容就会顺畅起来。

把今天学到的重写和协议各用代码实践一次,就会真正掌握。加油!

延伸阅读