编程语言导论/泛型多态
泛型多态的符号可以承担无限多种不同的类型。泛型多态主要分为两种:参数多态和子类型多态。在本章的剩余部分,我们将更详细地了解这些变体。
参数多态是例程、名称或符号的一个特性,这些例程、名称或符号可以以一个或多个类型为参数。这种多态性使我们能够定义通用的代码:它们可以被实例化以处理不同的类型。下面的代码展示了使用模板,这是在C++中实现参数多态的方式。
#include <iostream>
template <class T>
T GetMax (T a, T b) {
T result;
result = (a > b) ? a : b;
return (result);
}
int main() {
int i = 5, j = 6, k;
long l = 10, m = 5, n;
k = GetMax<int>(i, j); // type parameter: int
n = GetMax<long>(l, m); // type parameter: long
std::cout << k << std::endl;
std::cout << n << std::endl;
return 0;
}
上面的程序定义了一个名为GetMax
(第 3 行到第 8 行)的多态函数。在GetMax
的范围内定义的类型变量T
将在函数调用期间被实际类型替换。主函数展示了对GetMax
的两次调用。第 13 行的调用使用类型int
,而第 14 行的调用使用类型long
。GetMax
的参数使用“>
”运算符进行比较。因此,要使用此函数,必须将替换T的实际类型实现这种类型的比较。幸运的是,C++允许我们为自己的类型定义此运算符。例如,下面显示的用户定义类MyInt
是GetMax
的有效类型,因为它实现了大于运算符。
#include <iostream>
class MyInt {
friend std::ostream & operator<<(std::ostream& os, const MyInt& m) {
os << m.data;
}
friend bool operator >(MyInt& mi1, MyInt& mi2) {
return mi1.data > mi2.data;
}
public:
MyInt(int i) : data(i) {}
private:
const int data;
};
template <class T>
T GetMax (T a, T b) {
return (a > b) ? a : b;
}
int main () {
MyInt m1(50), m2(56);
MyInt mi = GetMax<MyInt>(m1, m2);
std::cout << mi << std::endl;
return 0;
}
参数多态存在于许多不同的静态类型语言中。例如,下面在 Java 中实现的函数操作一个泛型类型的列表。请注意,尽管C++和 Java 的语法类似,但这些语言中的参数多态以不同的方式实现。在C++模板中,参数函数的每个实例都是单独实现的。换句话说,C++编译器为多态函数的每个特化生成一个全新的函数。Java的泛型只为每个参数化函数创建一个实现。
public static <E> void printList(List<E> l) {
for (E e : l) {
System.out.println(e);
}
}
SML以类似于Java的方式实现参数多态。在整个程序中,每个参数函数只有一个实例存在。这些函数操作值的引用,而不是值本身。例如,下面的函数计算SML中泛型列表的长度。请注意,我们的实现不需要了解存储在列表中的值的任何信息。它只操作此列表的结构,将存储在其中的任何类型视为泛型引用。
- fun length nil = 0
= | length (_::t) = 1 + length t;
val length = fn : 'a list -> int
- length [1, 2, 3];
val it = 3 : int
- length [true, false, true];
val it = 3 : int
- length ["a", "bc", "def"];
val it = 3 : int
参数多态给了我们类型构造器的概念。类型构造器是一种接收类型并产生新类型的函数。例如,在上面的Java程序中,我们看到了类型构造器List<E>
。我们不能用这种类型实例化Java对象。相反,我们需要使用它的特化,例如List<Integer>
。因此,例如,用类型Integer
实例化List<E>
类似于将此类型传递给单参数函数List<E>
,该函数返回List<Integer>
。
参数多态是代码重用的重要机制。但是,并非每种编程语言都提供此功能。例如,广泛使用的语言(如C、Fortran或Pascal)中不存在参数多态。但是,仍然可以使用几种不同的策略来模拟它。例如,我们可以使用宏在C中模拟参数多态。下面的程序说明了此技术。宏SWAP
有一个类型参数,类似于类型构造器。我们已经实例化了此宏两次,第一次使用int
,然后使用char*
。
#include <stdio.h>
#define SWAP(T, X, Y) {T __aux = X; X = Y; Y = __aux;}
int main() {
int i0 = 0, i1 = 1;
char *c0 = "Hello, ", *c1 = "World!";
SWAP(int, i0, i1);
SWAP(char*, c0, c1);
printf("%d, %d\n", i0, i1);
printf("%s, %s\n", c0, c1);
}
在面向对象语言中存在一个众所周知的特性,即Liskov替换原则。该原则指出,在赋值左侧期望类型T的情况下,它也可以接收类型S,只要S是T的子类型。遵循Liskov替换原则的编程语言被认为提供了子类型多态。下面在Java中编写的程序说明了这种多态性。这三个类,String, Integer和LinkedList是Object的子类。因此,函数print可以接收作为实际参数的任何这些三个类的实例对象。
import java.util.LinkedList;
public class Sub {
public static void print(Object o) {
System.out.println(o);
}
public static void main(String[] a) {
print(new String("dcc024"));
print(new Integer(42));
print(new LinkedList<Integer>());
}
}
子类型多态之所以有效,是因为如果S是T的子类型,则S满足T所期望的契约。换句话说,类型T的任何属性也存在于其子类型中S。在上面的示例中,函数print期望知道如何将自身转换为字符串的类型。在Java中,任何具有属性toString()的类型都具有此知识。鉴于此属性存在于类Object中,它也存在于根据语言语义为Object.
是S的子类型。S是T。例如,下面的代码说明了Java编程语言中子类型的链。在Java中,关键字extends用于确定一个类是另一个类的子类型。
class Animal {
public void eat() {
System.out.println(this + " is eating");
}
public String toString () {
return "Animal";
}
}
class Mammal extends Animal {
public void suckMilk() {
System.out.println(this + " is sucking");
}
public void eat() {
suckMilk();
}
}
class Dog extends Mammal {
public void bark() {
System.out.println(this + " is barking");
}
public String toString () {
return "Dog";
}
}
创建子类型关系的另一种机制是结构化子类型。这种策略不如名义子类型常见。最著名的采用结构化子类型的编程语言之一是 ocaml。下面用这种语言编写的代码定义了两个对象,x和y。请注意,即使这些对象没有被显式声明为相同类型,但它们包含相同的接口,即它们都实现了方法get_x和set_x。因此,任何期望其中一个对象的代码都可以接收另一个对象。
let x =
object
val mutable x = 5
method get_x = x
method set_x y = x <- y
end;;
let y =
object
method get_x = 2
method set_x y = Printf.printf "%d\n" y
end;;
例如,函数let set_to_10 a = a#set_x 10;;
可以接收x或y,例如,set_to_10 x
和set_to_10 y
是有效的调用。事实上,任何提供属性set_x的对象都可以传递给set_to_10
,即使该对象与x或y的接口不相同。我们用下面的代码说明了最后一条语句:换句话说,如果对象O提供了另一个对象P的所有属性,那么我们说O是P。请注意,程序员不需要显式声明此子类型关系。
let z =
object
method blahblah = 2.5
method set_x y = Printf.printf "%d\n" y
end;;
set_to_10 z;;
一般来说,如果S的子类,则T的子类型,则S包含比T更多的属性。例如,在我们的类层次结构中,在Java中,Mammal的实例具有Animal的所有属性,并且除了这些属性之外,Mammal的实例还具有属性suckMilk,该属性不存在于Animal中。下图说明了这一事实。该图显示,类型的属性集是子类型属性集的子集。
但是,超类型的实例比子类型的实例多。如果S的子类,则T,则S的每个实例也是T的实例,反之则不成立。下图说明了此观察结果。