多态到底是什么?
多态性是指定义通用的数据结构或算法,以便将其用于多种数据类型。但完整的答案可能略有不同。本文我收集了各种形式的多态性,从您最可能已经使用过的常见类型到不太常见的类型,并比较了它们在面向对象或函数式语言中的表现。
参数多态性
这在很多语言中都是一种非常常见的技术,尽管它更广为人知的名字是“泛型”。其核心思想是允许程序员在定义数据结构时使用通配符类型,以便以后可以用任何类型填充。例如,在 Java 中,它是这样实现的:
class List<T> {
class Node<T> {
T data;
Node<T> next;
}
public Node<T> head;
public void pushFront(T data) { /* ... */ }
}
这T
是类型变量,因为您稍后可以“分配”任何您想要的类型:
List<String> myNumberList = new List<String>();
myNumberList.pushFront("foo");
myNumberList.pushFront(8) // Error: 8 is not a string
这里的列表只能包含字符串类型的元素,不能包含其他类型。如果我们试图违反此规则,会得到有用的编译器错误。此外,我们不必为每种可能的数据类型重新定义列表,因为我们可以针对所有可能的类型进行定义。
参数多态性不仅存在于命令式或面向对象语言中,在函数式编程中也很常见。例如,在 Haskell 中,列表定义如下:
data List a = Nil | Cons a (List a)
此定义的意思是:一个列表接受一个类型参数a
(等号左边的所有内容都定义了它的类型),它要么是一个空列表(Nil
),要么是一个 类型的元素a
和一个 类型的列表List a
。我们不需要任何外部pushFront
方法,因为第二个构造函数已经完成了这个操作:
let emptyList = Nil
oneElementList = Cons "foo" emptyList
twoElementList = Cons 8 oneElementList -- Error: 8 is not a string
特设多态性
这通常被称为函数或运算符重载。在允许这种重载的语言中,你可以多次定义一个函数来处理不同的输入类型。例如在 Java 中:
class Printer {
public String prettyPrint(int x) { /* ... */ }
public String prettyPrint(char c) { /* ... */ }
}
编译器会根据你传入的数据类型自动选择正确的方法。这可以让 API 更易于使用,因为你可以直接对任意类型调用同一个函数,而无需记住不同类型的一大堆变体(例如print_string
、print_int
等等)。
在 Haskell 中,Ad-hoc 多态性通过类型类实现。类型类有点像面向对象语言中的接口。例如,请参见下面的漂亮打印机示例:
class Printer p where
prettyPrint :: p -> String
instance Printer Int where
prettyPrint x = -- ...
instance Printer Char where
prettyPrint c = -- ...
亚型多态性
子类型更广为人知的名字是面向对象继承。经典的例子是车辆类型,如下面 Java 代码所示:
abstract class Vehicle {
abstract double getWeight();
}
class Car extends Vehicle {
double getWeight() { return 10.0; }
}
class Truck extends Vehicle {
double getWeight() { return 100.0; }
}
class Toyota extends Car { /* ... */ }
static void printWeight(Vehicle v) {
// Allowed because all vehicles have to have this method
System.out.println(v.getWeight());
}
这里我们可以使用车辆类的任何子类,就像它直接就是车辆类一样。注意,我们不能反过来,因为并非所有车辆都能保证是汽车。
当你被允许传递函数时,这种关系会变得有点复杂,例如在 Typescript 中:
const driveToyota = (c: Toyota) => { /* ... */ };
const driveVehicle = (c: Vehicle) => { /* ... */ };
function driveThis(f: (c: Car) => void): void { /* ... */ }
你可以将两个函数中的哪一个传递给driveThis
?你可能会想到第一个,毕竟正如我们上面所见,一个接受对象的函数也可以传递其子类(参见printWeight
方法)。但是,如果你传递一个函数,那就错了。你可以这样想:driveThis
想要一个可以接受任何汽车的函数。但是,如果你传入driveToyota
,该函数只能处理丰田汽车,这是不够的。另一方面,如果你传入一个可以驱动任何车辆的函数(driveVehicle
),这也包括汽车,所以driveThis
它会被接受。
由于 Haskell 不是面向对象的,因此子类型没有多大意义。
行多态性
现在我们来谈谈不太常用的多态类型,这些类型仅在极少数语言中实现。
行多态性就像子类型的小兄弟。我们允许指定行扩展:,而不是规定表单中的每个对象都是。这{ a :: A, b :: B }
使得它在函数中更容易使用,因为你不必考虑是否允许传递更具体的或更通用的类型,而只需检查你的类型是否与模式匹配。所以匹配但不匹配。这还有一个好处,就是你不会丢失信息。如果你将 a 强制转换为 a,你就丢失了该对象是哪款特定车辆的信息。使用行扩展,你可以保留所有信息。{ a :: A }
{ a :: A | r }
{ a :: A, b :: B }
{ a :: A | r }
{ b :: B, c :: C}
Car
Vehicle
printX :: { x :: Int | r } -> String
printX rec = show rec.x
printY :: { y :: Int | r } -> String
printY rec = show rec.y
-- type is inferred as `{x :: Int, y :: Int | r } -> String`
printBoth rec = printX rec ++ printY rec
实现行多态性的最流行的语言之一是PureScript,我目前也正在致力于将其引入 Haskell。
种类多态性
种类是类型的类型之一。我们都知道值,它是每个函数处理的数据。5
、"foo"
、false
都是值的例子。然后是类型级别,描述值。程序员应该非常熟悉这一点。前面三个值的类型分别是Int
、String
和Bool
。但在这之上还有一个级别:种类。所有类型的种类Type
也写作*
。所以这意味着5 :: Int :: Type
(::
表示“有类型”)。还有其他种类。例如,虽然我们之前的列表类型是一种类型 ( List a
),List
但没有 是什么a
?它仍然需要另一种类型作为参数来形成一个普通类型。因此它的种类是List :: Type -> Type
。如果你给出List
另一种类型(例如Int
),你会得到一个新的类型 ( List Int
)。
类型多态是指一个类型只定义一次,但仍可用于多种类型。最好的例子是Proxy
Haskell 中的数据类型。它用于用类型来“标记”一个值:
data Proxy a = ProxyValue
let proxy1 = (ProxyValue :: Proxy Int) -- a has kind `Type`
let proxy2 = (ProxyValue :: Proxy List) -- a has kind `Type -> Type`
高级多态性
有时,普通的临时多态性是不够的。使用临时多态性,你可以为不同类型的对象提供多种实现,API 的使用者可以选择使用哪种类型。但有时,作为 API 的提供者,你希望自己选择使用哪种实现。这时就需要高阶多态性了。在 Haskell 中,它如下所示:
-- ad-hoc polymorphism
f1 :: forall a. MyTypeClass a => a -> String
f1 = -- ...
-- higher-rank polymorphism
f2 :: Int -> (forall a. MyTypeClass a => a -> String) -> Int
f2 = -- ...
我们不再将forall
放在最外层,而是将其向内推,这样就声明了:传递一个可以处理任何a
实现 的类型的函数给我MyTypeClass
。你大概已经看出f1
就是这样一个函数,所以你可以将它传递给f2
。
一个标准的例子就是所谓的“ST-Trick”。它使得 Haskell 拥有无法脱离作用域的可变状态:
doSomething :: ST s Int
doSomething = do
ref <- newSTRef 10
x <- readSTRef ref
writeSTRef (x + 7)
readSTRef ref
这里的参数s
很重要,每个有状态操作s
在签名中都需要相同的参数。现在,关键在于将有状态计算转换为纯计算的函数runST
:
runST :: forall a. (forall s. ST s a) -> a
您可以看到,它s
是使用高阶多态创建的。由于我们没有对 的内容指定任何约束,因此s
状态计算除了在类型签名中传递它之外,无法对其进行任何操作。它的行为就像 的“标签” Proxy
。并且由于它仅在状态操作的范围内(由内部 定义forall
)定义,因此任何试图从计算中泄漏任何内容的行为都会在编译器中引发错误。
线性多态性
线性多态性与线性类型相关,线性类型又称跟踪数据“使用情况”的类型。线性类型跟踪某些数据的所谓多重性。通常,需要区分三种不同的多重性:“零”,表示仅存在于类型级别且不允许在值级别使用的数据;“一”,表示不允许重复的数据(文件描述符就是一个例子);“多”,表示所有其他数据。
线性类型对于保证资源利用率很有用。例如,fold
在函数式语言中,如果你覆盖一个可变数组。通常情况下,你必须在每一步都复制该数组,或者必须使用低级不安全函数。但使用线性类型,你可以保证该数组一次只能在一个地方使用,因此不会发生数据竞争。
多态性在类似上述函数中发挥作用fold
。如果你传入一个只使用一次参数的函数,那么整个函数fold
也只会使用一次初始值。如果你传入一个多次使用相同参数的函数,那么初始值也会被使用多次。线性多态允许你只定义一次函数,并且仍然提供这种保证。
线性类型类似于 Rust 的借用检查器,但 Rust 并不真正具备线性多态性。Haskell很快就会支持线性类型和多态性。
轻浮多态性
在 Haskell 中,所有普通数据类型都只是对堆的引用,就像在 Python 或 Java 中一样。但 Haskell 也允许使用所谓的“未提升”类型,例如直接使用机器整数。这意味着 Haskell 实际上在类型级别编码了数据类型的内存布局和位置(栈或堆)!这些可以用来进一步优化代码,这样 CPU 就不必先加载引用,然后再从(慢速的)RAM 中请求数据。
轻率多态性是指定义对提升类型和非提升类型都起作用的函数。
文章来源:https://dev.to/jvanbruegge/what-the-heck-is-polymorphism-nmh