Wielokrotna wysyłka - Multiple dispatch

Stwardnienie wysyłka lub multimethods jest cechą niektórych języków programowania , w którym funkcja lub metoda może być dynamicznie wysyłanych na podstawie run-time (dynamiczny) typu lub, w bardziej ogólnym przypadku, niektóre inne atrybut więcej niż jednego z jego argumentami . Jest to uogólnienie polimorfizmu pojedynczego wysyłania, w którym wywołanie funkcji lub metody jest wywoływane dynamicznie na podstawie typu pochodnego obiektu, na którym wywołano metodę. Wielokrotne rozsyłanie kieruje dynamiczną wysyłkę do implementującej funkcji lub metody przy użyciu połączonych cech jednego lub większej liczby argumentów.

Zrozumienie wysyłki

Twórcy oprogramowania komputerowego zazwyczaj organizują kod źródłowy w nazwane bloki, zwane różnie podprogramami , procedurami, podprogramami, funkcjami lub metodami. Kod w funkcji jest wykonywany przez wywołanie jej – wykonanie fragmentu kodu, który odwołuje się do jej nazwy . Przenosi to kontrolę tymczasowo na wywoływaną funkcję; po zakończeniu wykonywania funkcji sterowanie jest zazwyczaj przekazywane z powrotem do instrukcji w wywołującym, który następuje po odwołaniu.

Nazwy funkcji są zwykle wybierane tak, aby opisywały cel funkcji. Czasami pożądane jest nadanie kilku funkcjom tej samej nazwy, często dlatego, że wykonują one koncepcyjnie podobne zadania, ale operują na różnych typach danych wejściowych. W takich przypadkach odwołanie do nazwy w miejscu wywołania funkcji nie wystarcza do zidentyfikowania bloku kodu do wykonania. Zamiast tego liczba i typ argumentów wywołania funkcji są również używane do wyboru spośród kilku implementacji funkcji.

W bardziej konwencjonalnych, tj. obiektowych językach programowania z pojedynczą wysyłką , podczas wywoływania metody ( wysyłanie wiadomości w Smalltalk , wywoływanie funkcji składowej w C++ ), jeden z jej argumentów jest traktowany specjalnie i używany do określenia, który z (potencjalnie wiele) klas metod o tej nazwie. W wielu językach argument specjalny jest wskazywany składniowo; na przykład wiele języków programowania umieszcza specjalny argument przed kropką podczas wywoływania metody: special.method(other, arguments, here), aby lion.sound()wywołać ryk, podczas sparrow.sound()gdy ćwierkanie.

W przeciwieństwie do tego, w językach z wielokrotną wysyłką, wybrana metoda to po prostu ta, której argumenty odpowiadają numerowi i typowi wywołania funkcji. Nie ma specjalnego argumentu będącego właścicielem funkcji/metody wykonywanej w konkretnym wywołaniu.

Common Lisp Object systemu (CLOS) jest wczesnym i dobrze znanym przykładem wielokrotnego wysłania.

Typy danych

Podczas pracy z językami, które mogą rozróżniać typy danych w czasie kompilacji , może wtedy nastąpić wybór spośród alternatyw. Czynność tworzenia takich alternatywnych funkcji do wyboru czasu kompilacji jest zwykle określana jako przeciążenie funkcji.

W językach programowania, które odraczają identyfikację typu danych do czasu uruchomienia (tj. późnego wiązania ), wówczas musi nastąpić wybór spośród alternatywnych funkcji, na podstawie dynamicznie określanych typów argumentów funkcji. Funkcje, których alternatywne implementacje są wybierane w ten sposób, określane są najogólniej jako multimetody .

Z dynamicznym rozsyłaniem wywołań funkcji wiąże się pewien koszt w czasie wykonywania. W niektórych językach rozróżnienie między przeciążaniem a wieloma metodami może być zamazane, ponieważ kompilator określa, czy wybór czasu kompilacji można zastosować do danego wywołania funkcji, czy też potrzebne jest wolniejsze wysyłanie w czasie wykonywania.

Użyj w praktyce

Aby oszacować, jak często w praktyce stosuje się wielokrotną wysyłkę, Muschevici i in. badane programy wykorzystujące dynamiczną wysyłkę. Przeanalizowali dziewięć aplikacji, głównie kompilatorów, napisanych w sześciu różnych językach: Common Lisp Object System , Dylan , Cecil , MultiJava, Diesel i Nice. Ich wyniki pokazują, że 13-32% funkcji ogólnych używa dynamicznego typu jednego argumentu, podczas gdy 2,7-6,5% z nich używa dynamicznego typu wielu argumentów. Pozostałe 65-93% funkcji generycznych ma jedną konkretną metodę (przesłonięcie), a zatem uważa się, że nie używa się dynamicznych typów ich argumentów. Ponadto z badania wynika, że ​​2–20% funkcji ogólnych miało dwie, a 3–6% miało trzy implementacje funkcji konkretnych. Liczby szybko maleją dla funkcji z bardziej konkretnymi nadpisaniami.

Wysyłanie wielokrotne jest używane znacznie częściej w Julia , gdzie wielokrotne wysyłanie było główną koncepcją projektową od początku języka: zbierając te same statystyki co Muschevici na temat średniej liczby metod na funkcję ogólną, stwierdzono, że standardowa biblioteka Julia używa ponad dwukrotnie więcej niż w innych językach analizowanych przez Muschevici i ponad 10 razy w przypadku operatorów binarnych .

Dane z tych artykułów podsumowano w poniższej tabeli, w której współczynnik wysyłania DRjest średnią liczbą metod przypadających na funkcję ogólną; współczynnik wyboru CRjest średnią kwadratową liczby metod (w celu lepszego pomiaru częstości funkcji przy dużej liczbie metod); a stopień specjalizacji DoSto średnia liczba argumentów wyspecjalizowanych dla typu na metodę (tj. liczba argumentów, które są wysyłane na):

Język Średnia # metod (DR) Współczynnik wyboru (CR) Stopień specjalizacji (DoS)
Cecil 2,33 63.30 1,06
Wspólne seplenienie ( CMU ) 2,03 6,34 1,17
Wspólne seplenienie ( McCLIM ) 2,32 15.43 1,17
Common Lisp ( Stalowy Bank ) 2,37 26,57 1.11
Diesel 2,07 31,65 0,71
Dylan (Gwydion) 1,74 18.27 2.14
Dylan (OpenDylan) 2,51 43,84 1.23
Julia 5.86 51,44 1,54
Julia (tylko operatorzy) 28.13 78,06 2.01
MultiJava 1,50 8.92 1,02
Miły 1,36 3.46 0,33

Teoria

Teoria wielu języków dyspozytorskich została po raz pierwszy opracowana przez Castagna i in., definiując model funkcji przeciążonych z późnym wiązaniem . Zaowocowało to pierwszą formalizacją problemu kowariancji i kontrawariancji języków obiektowych oraz rozwiązaniem problemu metod binarnych.

Przykłady

Rozróżnienie wielokrotnych i pojedynczych wysyłek może być jaśniejsze na przykładzie. Wyobraź sobie grę, w której wśród obiektów (widocznych dla użytkownika) znajdują się statki kosmiczne i asteroidy. Kiedy zderzają się dwa obiekty, program może potrzebować zrobić różne rzeczy w zależności od tego, co właśnie uderzyło.

Języki z wbudowaną wielokrotną wysyłką

DO#

W C# wprowadzono obsługę dynamicznych multimetod w wersji 4 (kwiecień 2010) przy użyciu słowa kluczowego „dynamic”. Poniższy przykład ilustruje wiele metod, chociaż podobny efekt można osiągnąć za pomocą wyrażeń przełącznika od wersji 8 (wrzesień 2019). Podobnie jak wiele innych języków statycznych, C# obsługuje również przeciążanie metod statycznych. Microsoft spodziewa się, że w większości scenariuszy programiści wybiorą pisanie statyczne zamiast dynamicznego. Słowo kluczowe „dynamic” obsługuje współdziałanie z obiektami COM i dynamicznie typowanymi językami .NET.

class Program
{
    static void Main()
    {
        Console.WriteLine(Collider.Collide(new Asteroid(101),  new Spaceship(300)));
        Console.WriteLine(Collider.Collide(new Asteroid(10),   new Spaceship(10)));
        Console.WriteLine(Collider.Collide(new Spaceship(101), new Spaceship(10)));
    }
}

static class Collider
{
    public static string Collide(SpaceObject x, SpaceObject y) =>
        ((x.Size > 100) && (y.Size > 100)) ?
            "Big boom!" : CollideWith(x as dynamic, y as dynamic);
    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";
}

abstract class SpaceObject
{
    public SpaceObject(int size) => Size = size;

    public int Size { get; }
}

class Asteroid : SpaceObject
{
    public Asteroid(int size) : base(size) { }
}

class Spaceship : SpaceObject
{
    public Spaceship(int size) : base(size) { }
}

Wynik:

big-boom
a/s
s/s

Groovy

Groovy jest uniwersalnym Java kompatybilny / interusable JVM język, który, w przeciwieństwie do Javy, zastosowania późne wiązanie / stwardnienie wysyłkowy.

/*
    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 {}

Wspólne seplenienie

W języku z wielokrotnym wysyłaniem, takim jak Common Lisp , może to wyglądać mniej więcej tak (pokazano przykład Common Lisp):

(defmethod collide-with ((x asteroid) (y asteroid))
  ;; deal with asteroid hitting asteroid
  )
(defmethod collide-with ((x asteroid) (y spaceship))
  ;; deal with asteroid hitting spaceship
  )
(defmethod collide-with ((x spaceship) (y asteroid))
  ;; deal with spaceship hitting asteroid
  )
(defmethod collide-with ((x spaceship) (y spaceship))
  ;; deal with spaceship hitting spaceship
  )

i podobnie dla innych metod. Jawne testowanie i „rzucanie dynamiczne” nie są używane.

W obecności wielokrotnych wysyłek tradycyjna idea metod zdefiniowanych w klasach i zawartych w obiektach staje się mniej atrakcyjna — każda metoda kolidująca z powyższą jest przypisana do dwóch różnych klas, a nie do jednej. W związku z tym specjalna składnia wywoływania metod generalnie znika, więc wywoływanie metod wygląda dokładnie tak, jak wywoływanie zwykłych funkcji, a metody są pogrupowane nie w klasy, ale w funkcje ogólne .

Julia

Julia ma wbudowaną wielokrotną wysyłkę i ma kluczowe znaczenie dla projektowania języka. Przykładowa wersja Julii może wyglądać tak:

collide_with(x::Asteroid, y::Asteroid) = ... # deal with asteroid hitting asteroid
collide_with(x::Asteroid, y::Spaceship) = ... # deal with asteroid hitting spaceship
collide_with(x::Spaceship, y::Asteroid) = ... # deal with spaceship hitting asteroid
collide_with(x::Spaceship, y::Spaceship) = ... # deal with spaceship hitting spaceship

Powłoka nowej generacji

Next Generation Shell ma wbudowaną wielokrotną wysyłkę i wysyłkę predykatu i są one kluczowe w projektowaniu języka.

Metody o tej samej nazwie tworzą metodę wielokrotnego wysyłania, dlatego nie jest wymagana żadna specjalna deklaracja.

Gdy wywoływana jest metoda wielokrotnego wysyłania, metoda kandydująca jest przeszukiwana od dołu do góry. Za każdym razem, gdy typy argumentów są zgodne z typami określonymi dla parametrów, wywoływana jest metoda. Jest to w przeciwieństwie do wielu innych języków, w których wygrywa najbardziej konkretny mecz. Wewnątrz wywołanej metody, uszkodzony strażnik (gdzie warunek strażnika ma wartość false) powoduje, że wyszukiwanie metody do wywołania będzie kontynuowane.

{
	type SpaceObject
	type Asteroid(SpaceObject)
	type Spaceship(SpaceObject)
}

F init(o:SpaceObject, size:Int) o.size = size

F collide(x:Asteroid, y:Asteroid) "a/a"
F collide(x:Asteroid, y:Spaceship) "a/s"
F collide(x:Spaceship, y:Asteroid) "s/a"
F collide(x:Spaceship, y:Spaceship) "s/s"

F collide(x:SpaceObject, y:SpaceObject) {
	guard x.size > 100
	guard y.size > 100
	"big-boom"
}

echo(collide(Asteroid(101), Spaceship(300)))
echo(collide(Asteroid(10), Spaceship(10)))

Wynik:

big-boom
a/s

Raku

Raku , podobnie jak Perl, wykorzystuje sprawdzone pomysły z innych języków, a systemy typów okazały się oferować przekonujące korzyści w zakresie analizy kodu po stronie kompilatora i potężnej semantyki po stronie użytkownika poprzez wielokrotną wysyłkę.

Ma zarówno multimetody, jak i multisubs. Ponieważ większość operatorów to podprogramy, ma również wielu operatorów wysłanych.

Wraz ze zwykłymi ograniczeniami typu ma również ograniczenia where , które umożliwiają tworzenie bardzo wyspecjalizowanych podprogramów.

subset Mass of Real where 0 ^..^ Inf; 
role Stellar-Object {
    has Mass $.mass is required;
    method name () returns Str {...};
}
class Asteroid does Stellar-Object {
    method name () { 'an asteroid' }
}
class Spaceship does Stellar-Object {
    has Str $.name = 'some unnamed spaceship';
}
my Str @destroyed = < obliterated destroyed mangled >;
my Str @damaged = « damaged 'collided with' 'was damaged by' »;

# We add multi candidates to the numeric comparison operators because we are comparing them numerically,
# but makes no sense to have the objects coerce to a Numeric type.
# ( If they did coerce we wouldn't necessarily need to add these operators. )
# We could have also defined entirely new operators this same way.
multi sub infix:« <=> » ( Stellar-Object:D $a, Stellar-Object:D $b ) { $a.mass <=> $b.mass }
multi sub infix:« < » ( Stellar-Object:D $a, Stellar-Object:D $b ) { $a.mass < $b.mass }
multi sub infix:« > » ( Stellar-Object:D $a, Stellar-Object:D $b ) { $a.mass > $b.mass }
multi sub infix:« == » ( Stellar-Object:D $a, Stellar-Object:D $b ) { $a.mass == $b.mass }

# Define a new multi dispatcher, and add some type constraints to the parameters.
# If we didn't define it we would have gotten a generic one that didn't have constraints.
proto sub collide ( Stellar-Object:D $, Stellar-Object:D $ ) {*}

# No need to repeat the types here since they are the same as the prototype.
# The 'where' constraint technically only applies to $b not the whole signature.
# Note that the 'where' constraint uses the `<` operator candidate we added earlier.
multi sub collide ( $a, $b where $a < $b ) {
    say "$a.name() was @destroyed.pick() by $b.name()";
}
multi sub collide ( $a, $b where $a > $b ) {
    # redispatch to the previous candidate with the arguments swapped
    samewith $b, $a;
}

# This has to be after the first two because the other ones
# have 'where' constraints, which get checked in the
# order the subs were written. ( This one would always match. )
multi sub collide ( $a, $b ) {
    # randomize the order
    my ($n1, $n2) = ( $a.name, $b.name ).pick(*);
    say "$n1 @damaged.pick() $n2";
}

# The following two candidates can be anywhere after the proto,
# because they have more specialized types than the preceding three.

# If the ships have unequal mass one of the first two candidates gets called instead.
multi sub collide ( Spaceship $a, Spaceship $b where $a == $b ){
    my ($n1, $n2) = ( $a.name, $b.name ).pick(*);
    say "$n1 collided with $n2, and both ships were ",
    ( @destroyed.pick, 'left damaged' ).pick;
}

# You can unpack the attributes into variables within the signature.
# You could even have a constraint on them `(:mass($a) where 10)`.
multi sub collide ( Asteroid $ (:mass($a)), Asteroid $ (:mass($b)) ){
    say "two asteroids collided and combined into one larger asteroid of mass { $a + $b }";
}

my Spaceship $Enterprise .= new(:mass(1),:name('The Enterprise'));
collide Asteroid.new(:mass(.1)), $Enterprise;
collide $Enterprise, Spaceship.new(:mass(.1));
collide $Enterprise, Asteroid.new(:mass(1));
collide $Enterprise, Spaceship.new(:mass(1));
collide Asteroid.new(:mass(10)), Asteroid.new(:mass(5));

Rozszerzanie języków o biblioteki wielokrotnego wysyłania multiple

JavaScript

W językach, które nie obsługują wielokrotnej wysyłki na poziomie definicji języka lub składni, często można dodać wielokrotną wysyłkę za pomocą rozszerzenia biblioteki . JavaScript i TypeScript nie obsługują wielu metod na poziomie składni, ale możliwe jest dodanie wielu wysyłek za pośrednictwem biblioteki. Na przykład pakiet multimethod zapewnia implementację wielu ogólnych funkcji wysyłania.

Wersja wpisywana dynamicznie w 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
  }),
)

Wersja wpisana statycznie w 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
  }),
)

Pyton

Wiele wysyłek można dodać do Pythona za pomocą rozszerzenia biblioteki . Na przykład, używając modułu multimethod.py, a także modułu multimethods.py, który zapewnia multimetody w stylu CLOS dla Pythona bez zmiany podstawowej składni lub słów kluczowych języka.

from multimethods import Dispatch
from game_objects import Asteroid, Spaceship
from game_behaviors import as_func, ss_func, sa_func
collide = Dispatch()
collide.add_rule((Asteroid, Spaceship), as_func)
collide.add_rule((Spaceship, Spaceship), ss_func)
collide.add_rule((Spaceship, Asteroid), sa_func)
def aa_func(a, b):
    """Behavior when asteroid hits asteroid."""
    # ...define new behavior...
collide.add_rule((Asteroid, Asteroid), aa_func)
# ...later...
collide(thing1, thing2)

Funkcjonalnie jest to bardzo podobne do przykładu CLOS, ale składnia jest konwencjonalna w Pythonie.

Korzystając z dekoratorów Pythona 2.4 , Guido van Rossum stworzył przykładową implementację wielu metod z uproszczoną składnią:

@multimethod(Asteroid, Asteroid)
def collide(a, b):
    """Behavior when asteroid hits a asteroid."""
    # ...define new behavior...
@multimethod(Asteroid, Spaceship)
def collide(a, b):
    """Behavior when asteroid hits a spaceship."""
    # ...define new behavior...
# ... define other multimethod rules ...

a następnie przechodzi do definiowania dekoratora wielometodowego.

Pakiet PEAK-Rules zapewnia wielokrotną wysyłkę ze składnią podobną do powyższego przykładu. Został później zastąpiony przez PyProtocols.

Biblioteka Reg obsługuje również wysyłanie wielokrotne i predykatowe.

Emulacja wielokrotnej wysyłki

do

C nie ma dynamicznej wysyłki, więc musi być zaimplementowany ręcznie w jakiejś formie. Często wyliczenie służy do identyfikacji podtypu obiektu. Dystrybucję dynamiczną można przeprowadzić, wyszukując tę ​​wartość w tablicy rozgałęzień wskaźnika funkcji . Oto prosty przykład w 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 /* not a type of thing itself, instead used to find number of things */
} 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);
}

Dzięki bibliotece C Object System C obsługuje dynamiczną wysyłkę podobną do CLOS. Jest w pełni rozszerzalny i nie wymaga ręcznej obsługi metod. Dynamiczne wiadomości (metody) są wysyłane przez dyspozytora COS, który jest szybszy niż Objective-C. Oto przykład w COS:

#include <stdio.h>
#include <cos/Object.h>
#include <cos/gen/object.h>

// classes

defclass (Asteroid)
// data members
endclass

defclass (Spaceship)
// data members
endclass

// generics

defgeneric (_Bool, collide_with, _1, _2);

// multimethods

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

// example of use

int main(void)
{
  OBJ a = gnew(Asteroid);
  OBJ s = gnew(Spaceship);

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

  grelease(a);
  grelease(s);
}

C++

Od 2021 r. C++ natywnie obsługuje tylko pojedynczą wysyłkę, chociaż dodanie wielu metod (wielokrotnej wysyłki) zostało zaproponowane przez Bjarne Stroustrupa (i współpracowników) w 2007 r. Metody obejścia tego limitu są analogiczne: użyj wzorca odwiedzających , dynamicznej obsady lub biblioteka:

 // 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<Asteroid*>(&other)) {
             // handle Asteroid-Asteroid collision
         } else if (auto spaceship = dynamic_cast<Spaceship*>(&other)) {
             // handle Asteroid-Spaceship collision
         } else {
             // default collision handling here
         }
     }
 };

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

lub tabela odnośników do metody:

#include <cstdint>
#include <typeinfo>
#include <unordered_map>

class Thing {
  protected:
    Thing(std::uint32_t cid) : tid(cid) {}
    const std::uint32_t tid; // type id

    typedef void (Thing::*CollisionHandler)(Thing& other);
    typedef std::unordered_map<std::uint64_t, CollisionHandler> CollisionHandlerMap;

    static void addHandler(std::uint32_t id1, std::uint32_t id2, CollisionHandler handler) {
        collisionCases.insert(CollisionHandlerMap::value_type(key(id1, id2), handler));
    }
    static std::uint64_t key(std::uint32_t id1, std::uint32_t id2) {
        return std::uint64_t(id1) << 32 | id2;
    }

    static CollisionHandlerMap collisionCases;

  public:
    void collideWith(Thing& other) {
        auto handler = collisionCases.find(key(tid, other.tid));
        if (handler != collisionCases.end()) {
            (this->*handler->second)(other); // pointer-to-method call
        } else {
            // default collision handling
        }
    }
};

class Asteroid: public Thing {
    void asteroid_collision(Thing& other)   { /*handle Asteroid-Asteroid collision*/ }
    void spaceship_collision(Thing& other)  { /*handle Asteroid-Spaceship collision*/}

  public:
    Asteroid(): Thing(cid) {}
    static void initCases();
    static const std::uint32_t cid;
};

class Spaceship: public Thing {
    void asteroid_collision(Thing& other)   { /*handle Spaceship-Asteroid collision*/}
    void spaceship_collision(Thing& other)  { /*handle Spaceship-Spaceship collision*/}

  public:
    Spaceship(): Thing(cid) {}
    static void initCases();
    static const std::uint32_t cid; // class id
};

Thing::CollisionHandlerMap Thing::collisionCases;
const std::uint32_t Asteroid::cid = typeid(Asteroid).hash_code();
const std::uint32_t Spaceship::cid = typeid(Spaceship).hash_code();

void Asteroid::initCases() {
    addHandler(cid, cid, CollisionHandler(&Asteroid::asteroid_collision));
    addHandler(cid, Spaceship::cid, CollisionHandler(&Asteroid::spaceship_collision));
}

void Spaceship::initCases() {
    addHandler(cid, Asteroid::cid, CollisionHandler(&Spaceship::asteroid_collision));
    addHandler(cid, cid, CollisionHandler(&Spaceship::spaceship_collision));
}

int main() {
    Asteroid::initCases();
    Spaceship::initCases();

    Asteroid  a1, a2;
    Spaceship s1, s2;

    a1.collideWith(a2);
    a1.collideWith(s1);

    s1.collideWith(s2);
    s1.collideWith(a1);
}

Yomm2 biblioteka zapewnia szybki, prostopadły wdrażanie otwartych multimethods.

Składnia deklarowania metod otwartych jest inspirowana propozycją natywnej implementacji C++. Biblioteka wymaga, aby użytkownik rejestrował wszystkie klasy używane jako argumenty wirtualne (i ich podklasy), ale nie wymaga żadnych modyfikacji istniejącego kodu. Metody są zaimplementowane jako zwykłe wbudowane funkcje C++; mogą być przeciążone i mogą być przekazywane przez wskaźnik. Nie ma ograniczeń co do liczby argumentów wirtualnych i można je dowolnie mieszać z argumentami niewirtualnymi.

Biblioteka wykorzystuje kombinację technik (skompresowane tablice dyspozytorskie, doskonały skrót liczb całkowitych) do implementacji wywołań metod w stałym czasie, przy jednoczesnym ograniczeniu zużycia pamięci. Wysłanie wywołania otwartej metody z pojedynczym wirtualnym argumentem zajmuje tylko 15–30% więcej czasu niż wywołanie zwykłej wirtualnej funkcji składowej, gdy używany jest nowoczesny kompilator optymalizujący.

Przykład Asteroids można zaimplementować w następujący sposób:

#include <yorel/yomm2/cute.hpp>

using yorel::yomm2::virtual_;

class Thing {
  public:
    virtual ~Thing() {}
    // ...
};

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

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

register_class(Thing);
register_class(Spaceship, Thing);
register_class(Asteroid, Thing);

declare_method(void, collideWith, (virtual_<Thing&>, virtual_<Thing&>));

define_method(void, collideWith, (Thing& left, Thing& 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() {
    yorel::yomm2::update_methods();

    Asteroid  a1, a2;
    Spaceship s1, s2;

    collideWith(a1, a2);
    collideWith(a1, s1);

    collideWith(s1, s2);
    collideWith(s1, a1);
}

Stroustrup wspomina w The Design and Evolution of C++ , że podobała mu się koncepcja multimetod i rozważał wdrożenie jej w C++, ale twierdzi, że nie był w stanie znaleźć wydajnej przykładowej implementacji (porównywalnej z funkcjami wirtualnymi) i rozwiązać niektórych możliwych problemów z niejednoznacznością typów. Następnie stwierdza, że ​​chociaż ta funkcja nadal byłaby fajna, że ​​można ją w przybliżeniu zaimplementować za pomocą podwójnej wysyłki lub tabeli wyszukiwania opartej na typie, jak opisano w powyższym przykładzie C/C++, więc jest to funkcja o niskim priorytecie dla przyszłych wersji języka.

re

Od 2021 r., podobnie jak wiele innych języków programowania obiektowego, D natywnie obsługuje tylko pojedyncze wysyłanie. Możliwe jest jednak emulowanie otwartych multimetod jako funkcji bibliotecznej w D. Przykładem jest biblioteka openmethods .

// Declaration
Matrix plus(virtual!Matrix, virtual!Matrix);

// The override for two DenseMatrix objects
@method
Matrix _plus(DenseMatrix a, DenseMatrix b)
{
  const int nr = a.rows;
  const int nc = a.cols;
  assert(a.nr == b.nr);
  assert(a.nc == b.nc);
  auto result = new DenseMatrix;
  result.nr = nr;
  result.nc = nc;
  result.elems.length = a.elems.length;
  result.elems[] = a.elems[] + b.elems[];
  return result;
}

// The override for two DiagonalMatrix objects
@method
Matrix _plus(DiagonalMatrix a, DiagonalMatrix b)
{
  assert(a.rows == b.rows);
  double[] sum;
  sum.length = a.elems.length;
  sum[] = a.elems[] + b.elems[];
  return new DiagonalMatrix(sum);
}

Jawa

W języku z tylko pojedynczą wysyłką, takim jak Java , wielokrotna wysyłka może być emulowana z wieloma poziomami pojedynczej wysyłki:

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.
    }
}

instanceofMożna również użyć kontroli czasu wykonywania na jednym lub obu poziomach.

Wsparcie w językach programowania

Podstawowy paradygmat

Wspieranie ogólnych multimetod

Przez rozszerzenia

Zobacz też

Bibliografia

Linki zewnętrzne