Swift 与 Objective-C

重载 vs 重写:用 Swift 区分两者

重载和重写。这两个概念的名称相似到让人不禁疑惑,为什么要这样命名。

4 分钟阅读
重载 vs 重写:用 Swift 区分两者 封面图

重载和重写。这两个概念的名称相似到让人不禁疑惑,为什么要这样命名。

它们是考试和面试中的常见题目,而容易混淆的主要原因就是名称。两者的工作原理完全不同:一个是创建多个同名函数,另一个是重新定义继承来的函数。

它们的确定时机也不同:一个在编译时,另一个在运行时。这一区别还会延伸到静态分派和动态分派的性能讨论。

本文将通过 Swift 示例,清楚地区分这两个概念。

下面是核心总结。

  1. 重载:定义多个同名但参数不同的函数。与继承无关
  2. 重写:子类重新定义父类的方法。以继承为前提
  3. 重载在编译时决定调用哪个函数;重写在运行时决定
  4. 重写是多态的核心机制

重载:同一个名称定义多个函数

重载是指定义多个名称相同、但参数配置(数量、类型、标签)不同的函数。

func area(radius: Double) -> Double { .pi * radius * radius }
func area(width: Double, height: Double) -> Double { width * height }
func area(side: Double) -> Double { side * side }

编译器会根据调用处传入的参数判断使用哪个函数。写下 area(radius: 3) 的瞬间,就确定使用第一个函数了。在执行前就已经决定。

想想没有重载的 C 语言,就能体会它的便利。绝对值函数会根据类型使用不同名称:abs、labs 和 fabs。重载让相同概念可以使用相同名称。

Swift 在标准库的各处都使用重载,例如 print(:) 和 min(:_:)。通过运算符重载,一个 + 还可以处理整数加法、字符串拼接和数组合并。

有一点需要注意:最好避免只有返回类型不同的重载。Swift 在某些情况下允许这种语法,但如果调用处没有明确指定类型,就会出现歧义错误,也会让读者困惑。


重写:重新定义继承来的方法

重写是指子类使用相同的签名,重新定义从父类继承的方法。继承关系是必要前提。

class Animal {
    func speak() { print("...") }
}
class Dog: Animal {
    override func speak() { print("汪汪") }
}
class Cat: Animal {
    override func speak() { print("喵") }
}

关键就在这一幕。

let animals: [Animal] = [Dog(), Cat()]
for animal in animals {
    animal.speak() // 汪汪,喵
}

变量类型是 Animal,但实际执行的是实际实例的方法。调用哪个 speak,会在运行时查看真实对象后决定。这就是动态分派,也是多态运行的机制。

Swift 强制要求使用 override 关键字,这一点也值得注意。编译器会捕获误创建与父类方法签名相同的方法,或因拼写错误导致重写失败的情况。反过来,在父类中加上 final 就可以禁止重写。

即使变量类型是 Animal,执行的也是实际实例的方法
即使变量类型是 Animal,执行的也是实际实例的方法

用表格一次比较

分类 重载 重写
定义 同名,不同参数 重新定义相同签名
继承 不需要 必须
确定时机 编译时(静态) 运行时(动态)
目的 名称一致性、API 便利性 多态、替换行为
Swift 关键字 override(必须),可用 final 禁止

确定时机的差异也与性能有关。编译时确定的调用(静态分派)可以直接跳转,而运行时决定的调用(动态分派)必须经过实际类型的方法表。在 Swift 中,加上 final 或 private 后,编译器就有机会将动态分派转换为静态分派并进行优化。

重载在编译时决定,重写在运行时决定
重载在编译时决定,重写在运行时决定

不再混淆的记忆方法

记住英文单词的字面含义即可。

Overload = 超载。就是在同一个名称上“装载”多个函数。一个名称,多种功能。

Override = 忽略并踩过去。就是“覆盖”父类的定义。定义只有一个,但替换其内容。

面试时可以这样开始回答。

“重载是放置多个同名但参数不同的函数,在编译时决定;重写是重新定义继承来的方法,在运行时根据实际类型决定。创建多态的是重写。”


总结

  • 重载:定义多个同名、参数配置不同的函数。不需要继承
  • 重写:子类以相同签名重新定义父类方法。必须继承
  • 重载在编译时(静态)决定调用目标,重写则在运行时(动态)决定
  • 多态的核心机制是重写和动态分派
  • Swift 通过强制使用 override 关键字来防止错误,也可以用 final 禁止重写
  • 记忆方法:Overload 给名称加上多个功能,Override 覆盖原有定义

延伸阅读