面向对象编程/多态性
在编程语言和类型理论中,多态性是指为不同类型实体提供单个接口[1],或者使用单个符号来表示多个不同类型[2]。
最常识的重大多态性类别是
- 特设多态性:为任意一组单独指定的类型定义一个通用接口。
- 参数多态性:当一个或多个类型没有用名称指定,而是用可以表示任何类型的抽象符号指定时。
- 子类型化(也称为子类型多态性或包含多态性):当一个名称表示由某个共同超类关联的许多不同类的实例时[3]。
对多态类型系统的兴趣在 1960 年代得到显著发展,并在该十年末开始出现实际的实现。特设多态性和参数多态性最初在 Christopher Strachey 的编程语言的基本概念[4] 中有所描述,在那里它们被列为多态性的“两个主要类别”。特设多态性是 Algol 68 的一个特征,而参数多态性是 ML 类型系统的核心特征。
在 1985 年的一篇论文中,Peter Wegner 和 Luca Cardelli 引入了包含多态性一词来模拟子类型和继承[2],并引用 Simula 作为第一个实现它的编程语言。
Christopher Strachey 选择了特设多态性一词来指代可以应用于不同类型参数的多态函数,但其行为取决于应用它们的实际参数的类型(也称为函数重载或运算符重载)[5]。 在这种情况下,“特设”一词并非贬义;它只是指这种多态性不是类型系统的一个基本特征。 在下面的 Pascal / Delphi 示例中,Add
函数在查看调用时似乎在各种类型上通用,但实际上编译器将它们视为两个完全独立的函数,出于所有意图和目的
program Adhoc; function Add(x, y : Integer) : Integer; begin Add := x + y end; function Add(s, t : String) : String; begin Add := Concat(s, t) end; begin Writeln(Add(1, 2)); (* Prints "3" *) Writeln(Add('Hello, ', 'Mammals!')); (* Prints "Hello, Mammals!" *) end.
在动态类型语言中,情况可能更复杂,因为需要调用的正确函数可能只在运行时才能确定。
隐式类型转换也被定义为一种多态性,称为“强制转换多态性”[2][6]。
参数多态性允许以通用方式编写函数或数据类型,以便它可以统一处理值,而不依赖于其类型[7]。 参数多态性是使语言更具表现力的一种方式,同时仍然保持完全的静态类型安全性。
参数多态性的概念同时适用于数据类型和函数。 一个可以评估为或应用于不同类型值的函数被称为多态函数。 一个可以呈现为泛化类型(例如元素类型任意的列表)的数据类型被指定为多态数据类型,就像从其生成这种专门化的泛化类型一样。
参数多态性在函数式编程中无处不在,在那里它通常简称为“多态性”。 下面的 Haskell 示例显示了一个参数化的列表数据类型以及两个参数化的多态函数
data List a = Nil | Cons a (List a) length :: List a -> Integer length Nil = 0 length (Cons x xs) = 1 + length xs map :: (a -> b) -> List a -> List b map f Nil = Nil map f (Cons x xs) = Cons (f x) (map f xs)
参数多态性也存在于几种面向对象语言中。 例如,C++ 和 D 中的模板,或者在 C# 和 Java 中称为泛型
class List<T> { class Node<T> { T elem; Node<T> next; } Node<T> head; int length() { ... } } List<B> map(Func<A, B> f, List<A> xs) { ... }
John C. Reynolds(以及后来的 Jean-Yves Girard)正式地将这种多态性概念发展为对 lambda 演算的扩展(称为多态 lambda 演算或系统 F)。 任何参数化的多态函数必然在它可以做的事情方面受到限制,它作用于数据的形状而不是它的值,从而导致参数性的概念。
一些语言采用子类型化(也称为子类型多态性或包含多态性)来限制可以在多态性的特定情况下使用的类型的范围。 在这些语言中,子类型化允许编写一个函数来接受某个类型T的对象,但在传递属于类型S的对象时也能正常工作,而类型S是T的子类型(根据 Liskov 替换原则)。 这种类型关系有时写成S <: T。 相反,T被称为超类型S—写成T :> S。 子类型多态性通常在动态解析(见下文)。
在下面的示例中,我们将猫和狗设为动物的子类型。 过程letsHear()
接受一个动物,但如果传递一个子类型给它,它也能正常工作
abstract class Animal { abstract String talk(); } class Cat extends Animal { String talk() { return "Meow!"; } } class Dog extends Animal { String talk() { return "Woof!"; } } static void letsHear(final Animal a) { println(a.talk()); } static void main(String[] args) { letsHear(new Cat()); letsHear(new Dog()); }
在另一个例子中,如果Number、Rational 和Integer 是类型,使得Number :> Rational 和Number :> Integer,则编写用于接受Number 的函数在传递Integer 或Rational 时,与传递Number 时一样有效。 对象的实际类型可以对客户端隐藏在黑盒中,并通过对象标识访问。 事实上,如果Number 类型是抽象的,甚至可能无法获得其最派生类型为Number 的对象(参见抽象数据类型、抽象类)。 这种特定类型的类型层次结构被称为——尤其是在 Scheme 编程语言的上下文中——数值塔,并且通常包含更多类型。
面向对象编程语言使用子类化(也称为继承)提供子类型多态性。 在典型的实现中,每个类包含一个所谓的虚拟表——一个实现类接口的多态部分的函数表——每个对象包含一个指向其类的“vtable”的指针,然后在每次调用多态方法时查询该指针。 这种机制是以下内容的一个示例
- 后期绑定,因为虚拟函数调用直到调用时才绑定;
- 单分派(即单参数多态性),因为虚拟函数调用只是通过查看第一个参数(
this
对象)提供的 vtable 来绑定,因此其他参数的运行时类型完全无关。
大多数其他流行的对象系统也是如此。 但是,有些系统,比如 Common Lisp Object System,提供了多重分派,在多重分派下,方法调用在所有参数中都是多态的。
参数多态性和子类型化之间的相互作用导致了方差和限定量化的概念。
行多态性[8] 是一个类似于子类型化但不同的概念。 它处理结构类型。 它允许使用所有类型具有某些属性的值,而不会丢失剩余的类型信息。
一个相关的概念是多类型性(或数据类型泛型)。 多类型函数比多态函数更通用,在这种函数中,“虽然可以为特定数据类型提供固定的特设情况,但特设组合器不存在”[9]。
多态性可以通过实现选择的时间来区分:静态地(在编译时)或动态地(在运行时,通常通过虚函数)。这分别被称为静态分派和动态分派,相应的多态形式也相应地被称为静态多态和动态多态。
静态多态执行速度更快,因为没有动态分派开销,但需要额外的编译器支持。此外,静态多态允许编译器进行更深入的静态分析(特别是为了优化)、源代码分析工具和人类读者(程序员)。动态多态更加灵活但速度更慢——例如,动态多态允许鸭子类型,动态链接库可以操作对象而不知道它们的完整类型。
静态多态通常发生在特设多态和参数化多态中,而动态多态在子类型多态中很常见。然而,通过更巧妙地使用模板元编程,特别是奇异递归模板模式,可以实现带子类型的静态多态。