多分派

多分派或译多重派发(multiple dispatch)或多方法(multimethod),是某些编程语言的一个特性,其中的函数或者方法,可以在运行时间(动态的)基于它的实际参数的类型,或在更一般的情况下于此之外的其他特性,来动态分派。多分派是对单分派多态的推广,这里的函数或方法调用,基于在其上调用方法的对象的派生类型,而动态分派。多分派使用一个或多个实际参数的组合特征,路由动态分派至实现函数或方法。

理解分派
软件工程师通常把代码写进代码块中,代码块通常称作过程、函数、方法。代码通过被调用来执行,调用时将控制权传入函数中,当函数执行完成后将控制权返回给调用者。

函数名通常用来描述函数的目的。有时会将多个函数起同样的名称。比如同名函数在逻辑上处理相同的任务,但是操作在不同类型的输入值上。在这种情况下,无法仅仅通过函数名,来判断目标代码块。那么,函数的实际参数的个数和类型,也就被用来判断。

通常,单分派面向对象语言,在调用一个方法时,方法参数中一个参数会被特殊对待,并用来决定哪一个方法(如果有多个同名方法)会被调用。在许多语言中,这个特殊的参数是在语法上指明的,许多编程语言在调用方法时,把特殊参数放在小圆点(.)之前。例如 special.method(other, arguments, here),这样 lion.sound() 将会发出狮吼,同时 sparrow.sound() 只会吱吱地叫。一般来说,对于面向对象的编程语言,这个小圆点之前的参数(上例中的lion和sparrow)被称为接收者。

相反,在实现了多分派的语言中,被调用的函数,即是那些参数个数一样多,并且类型也匹配的调用。在调用中并没有特殊参数,来决定那个方法被调用。也就是说,所有参数的运行时类型都参与分派。CLOS是早期和著名的多分派语言。

数据类型
对于编译时间可以区分数据类型的编程语言,在(alternative)函数中进行选择,可以发生在编译时间,创建交替函数用于编译时间选择的活动,通常被叫做函数重载。

在有些编程语言中,这种数据类型的识别,可以被延后至运行时间()。交替函数的选择发生在运行时间,并依据动态确定的函数实际参数的类型。以这种方式选择交替实现的函数,通常被称为多方法。

理论
多分派语言的理论首先由Castagna等人开发,它为具有的重载函数定义了模型。它产生了面向对象语言的协变与逆变问题的首次形式化,和二元方法(binary method)问题的一种解法。

例子
可以通过例子更加清晰的区分多分派和单一分派。假想一个游戏,它有两种(用户可见的)物体:小行星和太空船;当两个物体要相撞的时候,程序需要依据什么物体要相撞而做不同的事情。

内建多分派
Common Lisp
在具有多分派的Common Lisp语言中,可以在Common Lisp对象系统中如下这样实现:

(defclass asteroid () ((size :reader size :initarg :size)))
(defclass spaceship () ((size :reader size :initarg :size)))
(defun space-object (class size) (make-instance class :size size))

; collide-with是具有多分派的泛化函数
(defmethod collide-with ((x asteroid) (y asteroid)) "a/a")
(defmethod collide-with ((x asteroid) (y spaceship)) "a/s")
(defmethod collide-with ((x spaceship) (y asteroid)) "s/a")
(defmethod collide-with ((x spaceship) (y spaceship)) "s/s")

(defun collide (x y)
(if (and (> (size x) 100) (> (size y) 100))
"big-boom"
(collide-with x y)))

(print (collide (space-object 'asteroid 101) (space-object 'spaceship 300)))
(print (collide (space-object 'asteroid 10) (space-object 'spaceship 10)))
(print (collide (space-object 'spaceship 101) (space-object 'spaceship 10)))

并且对其他方法也是类似的。没有使用显式测试和“动态转换”。

在这里的宏defclass定义里的槽指定(size :reader size :initarg :size)中,首位的size是槽名字,其后是2个槽选项::reader size指定在名为size的泛化函数上定义一个无限定符(unqualified)方法来读取这个槽的值,:initarg :size声明一个名为:size的初始化参数并指定这个初始化参数初始化这个槽。在这里的标准泛化函数make-instance应用(make-instance class :size size)的参数中,class是要实例化的类,:size size是由成对的名字与值构成的初始化参数列表。

由于多分派的存在,方法要定义在类中并包含在对象中的传统想法,变得不再吸引人了,上述每个collide-with方法,都附属于两个不同的类而非一个类。因此方法调用的特殊语法,一般会消失,从而方法调用看起来完全就像正常的函数调用,并且方法被组织入泛化函数而非类中。

Julia
Julia有内建的多分派,并且它是语言设计的中心。Julia版本的例子如下:

abstract type SpaceObject end

struct Asteroid 100 && y.size > 100 ? "Big boom!" : collide_with(x, y)

输出:

julia> collide(Asteroid(101), Spaceship(300))
"Big boom!"

julia> collide(Asteroid(10), Spaceship(10))
"a/s"

julia> collide(Spaceship(101), Spaceship(10))
"s/s"

C#
C#在版本4(2010年4月),使用关键字dynamic,介入了对动态多方法的支持。下面的例子展示多方法。像很多其他静态类型的语言语言一样,C#还支持静态方法重载,Microsoft预期开发者在多数场景下会选用静态类型超过动态类型。dynamic关键字支持COM对象和动态类型的.NET语言的互操作。

下面的例子使用了C# 9和C# 10介入的特征比如记录类型:

using static ColliderLibrary;

Console.WriteLine(Collide(new Asteroid(101), new Spaceship(300)));
Console.WriteLine(Collide(new Asteroid(10), new Spaceship(10)));
Console.WriteLine(Collide(new Spaceship(101), new Spaceship(10)));

string Collide(SpaceObject x, SpaceObject y) =>
x.Size > 100 && y.Size > 100
? "Big boom!"
: CollideWith(x as dynamic, y as dynamic); // Dynamic dispatch to CollideWith method

class ColliderLibrary {
public static string CollideWith(Asteroid x, Asteroid y) => "a/a";
public static string CollideWith(Asteroid x, Spaceship y) => "a/s";
public static string CollideWith(Spaceship x, Asteroid y) => "s/a";
public static string CollideWith(Spaceship x, Spaceship y) => "s/s";
}

abstract record SpaceObject(int Size);
record Asteroid(int Size) : SpaceObject(Size);
record Spaceship(int Size) : SpaceObject(Size);

输出:

Big boom!
a/s
s/s

Groovy
Groovy是兼容/互用于Java的通用的JVM语言,它对立于Java,使用后期绑定/多分派。

/*
Groovy implementation of C# example above
Late binding works the same when using non-static methods or compiling class/methods statically
(@CompileStatic annotation)
*/
class Program {
static void main(String[] args) {
println Collider.collide(new Asteroid(101), new Spaceship(300))
println Collider.collide(new Asteroid(10), new Spaceship(10))
println Collider.collide(new Spaceship(101), new Spaceship(10))
}
}

class Collider {
static String collide(SpaceObject x, SpaceObject y) {
(x.size > 100 && y.size > 100) ? "big-boom" : collideWith(x, y) // Dynamic dispatch to collideWith method
}

private static String collideWith(Asteroid x, Asteroid y) { "a/a" }
private static String collideWith(Asteroid x, Spaceship y) { "a/s" }
private static String collideWith(Spaceship x, Asteroid y) { "s/a" }
private static String collideWith(Spaceship x, Spaceship y) { "s/s"}
}

class SpaceObject {
int size
SpaceObject(int size) { this.size = size }
}

@InheritConstructors class Asteroid extends SpaceObject {}
@InheritConstructors class Spaceship extends SpaceObject {}

库扩展多分派
在不于语言定义或语法层次支持多分派的语言中,可能经常使用扩展库来增加多分派。

Python
Python可以使用第三方库扩展来增加多分派。例如,最早提出的模块multimethods.py,提供了CLOS风格的多方法而不用变更语言的底层语法或关键字。Guido van Rossum使用Python 2.4介入的修饰器(decorator),建立了具有简化语法的多方法的简单实现。multipledispatch库采用的形式与之一致。

模块multimethod,采用了修饰器和Python 3.5介入的类型提示实现多方法,下面的多方法例子还基于了Python 3.10介入的联合类型(types.UnionType):

from multimethod import multimethod

class Asteroid():
def __init__(self, size=0):
self.size = size

class Spaceship():
def __init__(self, size=0):
self.size = size

SpaceObject = Asteroid | Spaceship

@multimethod
def collide_with(x :Asteroid, y :Asteroid):
return "a/a"

@multimethod
def collide_with(x :Asteroid, y :Spaceship):
return "a/s"

@multimethod
def collide_with(x :Spaceship, y :Asteroid):
return "s/a"

@multimethod
def collide_with(x :Spaceship, y :Spaceship):
return "s/s"

def collide(x :SpaceObject, y :SpaceObject):
assert isinstance(x, SpaceObject) \
and isinstance(y, SpaceObject)
print("Big boom!" if x.size>100 and y.size>100
else collide_with(x, y))

其执行:

>> collide(Asteroid(101), Spaceship(300))

Big boom!
>>> collide(Asteroid(10), Spaceship(10))
a/s
>>> collide(Spaceship(101), Spaceship(10))
s/s

plum库也实现了这种形式的多分派。

JavaScript
JavaScript和TypeScript不在语言语法层次上支持多方法,但可以通过第三方库扩展来增加多分派。例如,使用multimethod包,它提供了多分派、泛化函数的实现。这个库的JavaScript的动态类型版本例子:

import { multi, method } from '@arrows/multimethod'

class Asteroid {}
class Spaceship {}

const collideWith = multi(
method([Asteroid, Asteroid], (x, y) => {
// deal with asteroid hitting asteroid
}),
method([Asteroid, Spaceship], (x, y) => {
// deal with asteroid hitting spaceship
}),
method([Spaceship, Asteroid], (x, y) => {
// deal with spaceship hitting asteroid
}),
method([Spaceship, Spaceship], (x, y) => {
// deal with spaceship hitting spaceship
}),
)

这个库还有TypeScript有对应的静态类型版本。{{efn|
TypeScript的多方法示例:

import { multi, method, Multi } from '@arrows/multimethod'

class Asteroid {}
class Spaceship {}

type CollideWith = Multi & {
(x: Asteroid, y: Asteroid): void
(x: Asteroid, y: Spaceship): void
(x: Spaceship, y: Asteroid): void
(x: Spaceship, y: Spaceship): void
}

const collideWith: CollideWith = multi(
method([Asteroid, Asteroid], (x, y) => {
// deal with asteroid hitting asteroid
}),
method([Asteroid, Spaceship], (x, y) => {
// deal with asteroid hitting spaceship
}),
method([Spaceship, Asteroid], (x, y) => {
// deal with spaceship hitting asteroid
}),
method([Spaceship, Spaceship], (x, y) => {
// deal with spaceship hitting spaceship
}),
)
}}

C++
用C++17编写的YOMM2第三方库,提供了开放多方法的快速且正交性的实现。上述例子可通过它实现为:

#include

import std;

using std::unique_ptr;

class Collideable {
public:
virtual ~Collideable() = default;
};

class Asteroid : public Collideable {
// ...
};

class Spaceship : public Collideable {
// ...
};

register_classes(Collideable, Spaceship, Asteroid);

declare_method(void, collideWith, (virtual_, virtual_));

define_method(void, collideWith, (Collideable& left, Collideable& right)) {
// default collision handling
}

define_method(void, collideWith, (Asteroid& left, Asteroid& right)) {
// handle Asteroid-Asteroid collision
}

define_method(void, collideWith, (Asteroid& left, Spaceship& right)) {
// handle Asteroid-Spaceship collision
}

define_method(void, collideWith, (Spaceship& left, Asteroid& right)) {
// handle Spaceship-Asteroid collision
}

define_method(void, collideWith, (Spaceship& left, Spaceship& right)) {
// handle Spaceship-Spaceship collision
}

int main(int argc, char* argv[]) {
yorel::yomm2::update_methods();

unique_ptr a1(std::make_unique());
unique_ptr a2(std::make_unique());
unique_ptr s1(std::make_unique());
unique_ptr s2(std::make_unique());
// note: types partially erased

collideWith(a1, a2); // Asteroid-Asteroid collision
collideWith(a1, s1); // Asteroid-Spaceship collision
collideWith(s1, a1); // Spaceship-Asteroid collision
collideWith(s1, s2); // Spaceship-Spaceship collision

return 0;
}

模拟多分派
C
C语言没有动态分派,它必须手工的以某种形式实现。经常使用一个enum来识别一个对象的子类型。动态分派可以在一个函数指针分支表中查找这个值来完成。下面是采用C语言的简单例子:

typedef void (*CollisionCase)(void);

void collision_AA(void)
{ / handle Asteroid-Asteroid collision / };
void collision_AS(void)
{ / handle Asteroid-Spaceship collision / };
void collision_SA(void)
{ / handle Spaceship-Asteroid collision / };
void collision_SS(void)
{ / handle Spaceship-Spaceship collision/ };

typedef enum {
THING_ASTEROID = 0,
THING_SPACESHIP,
THING_COUNT / 它自身不是事物类型,转而用来表示事物的数目 /
} Thing;

CollisionCase collisionCases[THING_COUNT][THING_COUNT] = {
{&collision_AA, &collision_AS},
{&collision_SA, &collision_SS}
};

void collide(Thing a, Thing b)
{
(*collisionCases[a][b])();
}

int main(void)
{
collide(THING_SPACESHIP, THING_ASTEROID);
}

C语言使用C Object System库,可以支持类似于CLOS的动态分派。它是完全可扩展的并且方法不需要任何的手工处理。动态消息(方法)通过COS分派器来分派,它比Objective-C更快。{{efn|下面是使用COS的例子:

#include
#include
#include

//
defclass (Asteroid)
/ 数据成员 /
endclass

defclass (Spaceship)
/ 数据成员 /
endclass

/ 泛化函数 /
defgeneric (_Bool, collide_with, _1, _2);

/ 多方法 /
defmethod (_Bool, collide_with, Asteroid, Asteroid)
/ deal with asteroid hitting asteroid /
endmethod

defmethod (_Bool, collide_with, Asteroid, Spaceship)
/ deal with asteroid hitting spaceship /
endmethod

defmethod (_Bool, collide_with, Spaceship, Asteroid)
/ deal with spaceship hitting asteroid /
endmethod

defmethod (_Bool, collide_with, Spaceship, Spaceship)
/ deal with spaceship hitting spaceship /
endmethod

/ 用例 /
int main(int argc, char *argv[])
{
OBJ a = gnew(Asteroid);
OBJ s = gnew(Spaceship);

printf(" = %d\n", collide_with(a, a));
printf(" = %d\n", collide_with(a, s));
printf(" = %d\n", collide_with(s, a));
printf(" = %d\n", collide_with(s, s));

grelease(a);
grelease(s);
}

}}

C++
截至目前,C++本身只支持单分派,下面是使用dynamic_cast来模拟多分派的例子:

// Example using run time type comparison via dynamic_cast

struct Thing {
virtual void collideWith(Thing& other) = 0;
};

struct Asteroid : Thing {
void collideWith(Thing& other) {
// dynamic_cast to a pointer type returns NULL if the cast fails
// (dynamic_cast to a reference type would throw an exception on failure)
if (auto asteroid = dynamic_cast(&other)) {
// handle Asteroid-Asteroid collision
} else if (auto spaceship = dynamic_cast(&other)) {
// handle Asteroid-Spaceship collision
} else {
// default collision handling here
}
}
};

struct Spaceship : Thing {
void collideWith(Thing& other) {
if (auto asteroid = dynamic_cast(&other)) {
// handle Spaceship-Asteroid collision
} else if (auto spaceship = dynamic_cast(&other)) {
// handle Spaceship-Spaceship collision
} else {
// default collision handling here
}
}
};

Java
在Java这种只有单分派但支持方法重载的语言中,多分派可以通过多层单分派来模拟:

interface Collideable {
void collideWith(final Collideable other);

/ These methods would need different names in a language without method overloading. /
void collideWith(final Asteroid asteroid);
void collideWith(final Spaceship spaceship);
}

class Asteroid implements Collideable {
public void collideWith(final Collideable other) {
// Call collideWith on the other object.
other.collideWith(this);
}

public void collideWith(final Asteroid asteroid) {
// Handle Asteroid-Asteroid collision.
}

public void collideWith(final Spaceship spaceship) {
// Handle Asteroid-Spaceship collision.
}
}

class Spaceship implements Collideable {
public void collideWith(final Collideable other) {
// Call collideWith on the other object.
other.collideWith(this);
}

public void collideWith(final Asteroid asteroid) {
// Handle Spaceship-Asteroid collision.
}

public void collideWith(final Spaceship spaceship) {
// Handle Spaceship-Spaceship collision.
}
}

还可以在一层或两层上使用运行时instanceof检查。

Python
在Python中可以通过采用标准库functools中的singledispatchmethod,将方法变换为单一分派泛化函数,这个实际上是的多分派例子还可以实现为:

from functools import singledispatchmethod

class Asteroid():
def __init__(self, size=0):
self.size = size
@singledispatchmethod
def collide_with(self, other): pass

class Spaceship():
def __init__(self, size=0):
self.size = size
@singledispatchmethod
def collide_with(self, other): pass

SpaceObject = Asteroid | Spaceship

@Asteroid.collide_with.register
def _(self, other :Asteroid):
return "a/a"
@Asteroid.collide_with.register
def _(self, other :Spaceship):
return "a/s"

@Spaceship.collide_with.register
def _(self, other :Asteroid):
return "s/a"
@Spaceship.collide_with.register
def _(self, other :Spaceship):
return "s/s"

def collide(x, y):
assert isinstance(x, SpaceObject) \
and isinstance(y, SpaceObject)
print("Big boom!" if x.size>100 and y.size>100
else x.collide_with(y))

下面的例子通过命名约定采用了反射式实现方式模拟多分派:

class SpaceObject():
def collide_with(self, *args):
key = ('_'+type(self).__name__
+*.join('_'+type(i).__name__ for i in args)
+'__collide')
return (self.__getattribute__(key)(*args)
if key in self.__dir__() else None)

class Asteroid(SpaceObject):
def __init__(self, size=0):
self.size = size
def _Asteroid_Asteroid__collide(self, other):
return "a/a"
def _Asteroid_Spaceship__collide(self, other):
return "a/s"

class Spaceship(SpaceObject):
def __init__(self, size=0):
self.size = size
def _Spaceship_Asteroid__collide(self, other):
return "s/a"
def _Spaceship_Spaceship__collide(self, other):
return "s/s"

def collide(x, y):
assert isinstance(x, SpaceObject) \
and isinstance(y, SpaceObject)
print("Big boom!" if x.size>100 and y.size>100
else x.collide_with(y))

其执行:

>> collide(Spaceship(101), Asteroid(300))

Big boom!
>>> collide(Asteroid(10), Asteroid(10))
a/a
>>> collide(Spaceship(101), Asteroid(10))
s/a

编程语言支持
主范型

  • Julia

支持通用的多方法
*
*

  • Clojure
  • Common Lisp(通过Common Lisp对象系统)
  • Dylan

*

  • Groovy

*

  • Nim,在v0.20之后泛化方法被废止,使用多方法需要加编译指令
  • Raku
  • R

*
*

  • VB.Net,通过后期绑定,还通过.Net DLR
  • Wolfram语言,通过符号模式匹配

*

通过扩展

  • 任何.NET语言(通过库MultiMethods.NET)
  • C(通过库C Object System)
  • C++(通过库yomm2和omm)
  • D(通过库openmethods)
  • Factor(通过标准multimethods词汇表)
  • Java(使用扩展MultiJava)
  • JavaScript(通过包@arrows/multimethod)
  • Python(模块multimethod)
  • Ruby(通过Multiple Dispatch库、Multimethod包和Vlx-Multimethods包)
  • Scheme(通过TinyCLOS)
  • TypeScript(通过包@arrows/multimethod)

代码示例
参见
*
*访问者模式

引用
外部链接
*
*

评论 (0)

  • 还没有评论,来抢沙发吧。