Wysyłka dynamiczna - Dynamic dispatch

W informatyce , dynamiczny wysyłkowy jest proces wyboru, który wdrożenie polimorficznych pracy ( metoda lub funkcja) zadzwonić w czasie wykonywania . Jest powszechnie stosowany i uważany za główną cechę języków i systemów programowania obiektowego (OOP).

Systemy zorientowane obiektowo modelują problem jako zbiór oddziałujących ze sobą obiektów, które wykonują operacje określone nazwą. Polimorfizm to zjawisko, w którym każdy z nieco wymiennych obiektów ujawnia operację o tej samej nazwie, ale prawdopodobnie różniącej się zachowaniem. Na przykład obiekt File i obiekt Database mają metodę StoreRecord , której można użyć do zapisania rekordu personelu w pamięci. Ich implementacje są różne. Program przechowuje odniesienie do obiektu, który może być albo obiektem File, albo obiektem bazy danych . Który to może być określony przez ustawienie czasu wykonywania, a na tym etapie program może nie wiedzieć o którym. Kiedy program wywołuje StoreRecord na obiekcie, coś musi wybrać, które zachowanie zostanie wprowadzone. Jeśli ktoś myśli o OOP jako o wysyłaniu komunikatów do obiektów, to w tym przykładzie program wysyła komunikat StoreRecord do obiektu nieznanego typu, pozostawiając systemowi wsparcia wykonawczego wysłanie komunikatu do właściwego obiektu. Obiekt realizuje dowolne zachowanie, które implementuje.

Dystrybucja dynamiczna kontrastuje z wysyłaniem statycznym , w którym implementacja operacji polimorficznej jest wybierana w czasie kompilacji . Celem dynamicznej wysyłki jest odroczenie wyboru odpowiedniej implementacji do momentu poznania typu czasu wykonania parametru (lub wielu parametrów).

Wysyłanie dynamiczne różni się od późnego wiązania (nazywanego również wiązaniem dynamicznym). Wiązanie nazw wiąże nazwę z operacją. Operacja polimorficzna ma kilka implementacji, wszystkie powiązane z tą samą nazwą. Powiązania można tworzyć w czasie kompilacji lub (z późnym wiązaniem) w czasie wykonywania. W przypadku dynamicznej wysyłki w czasie wykonywania wybierana jest jedna konkretna implementacja operacji. Podczas gdy dynamiczna wysyłka nie oznacza późnego wiązania, późne wiązanie oznacza dynamiczną wysyłkę, ponieważ implementacja operacji z późnym wiązaniem nie jest znana do czasu wykonywania.

Wysyłka pojedyncza i wielokrotna

Wybór wersji metody do wywołania może opierać się na pojedynczym obiekcie lub kombinacji obiektów. Pierwsza z nich nazywana jest pojedynczą wysyłką i jest bezpośrednio obsługiwana przez popularne języki obiektowe, takie jak Smalltalk , C++ , Java , C# , Objective-C , Swift , JavaScript i Python . W tych i podobnych językach można wywołać metodę dzielenia o składni przypominającej

dividend.divide(divisor)  # dividend / divisor

gdzie parametry są opcjonalne. Jest to rozumiane jako wysłanie wiadomości o nazwie dziel z parametrem dzielnik do dywidendy . Implementacja zostanie wybrana wyłącznie na podstawie typu dywidendy (może być wymierna , zmiennoprzecinkowa , macierzowa ), bez względu na typ lub wartość dzielnika .

W przeciwieństwie do tego, niektóre języki wysyłają metody lub funkcje oparte na kombinacji operandów; w przypadku podziału typy dywidendy i dzielnika wspólnie określają, która operacja dzielenia zostanie wykonana. Nazywa się to wielokrotną wysyłką . Przykładami języków obsługujących wielokrotne wysyłanie są Common Lisp , Dylan i Julia .

Mechanizmy dynamicznej wysyłki

Język może być zaimplementowany z różnymi mechanizmami dynamicznej wysyłki. Wybory dynamicznego mechanizmu dyspozytorskiego oferowanego przez język w dużej mierze zmieniają paradygmaty programowania, które są dostępne lub są najbardziej naturalne w użyciu w danym języku.

Normalnie, w języku typowanym, mechanizm wysyłki będzie wykonywany na podstawie typu argumentów (najczęściej na podstawie typu odbiorcy wiadomości). Języki ze słabymi systemami wpisywania lub bez systemów typowania często zawierają tabelę rozsyłania jako część danych obiektu dla każdego obiektu. Pozwala to na zachowanie instancji, ponieważ każda instancja może mapować daną wiadomość na oddzielną metodę.

Niektóre języki oferują podejście hybrydowe.

Wysyłka dynamiczna zawsze wiąże się z obciążeniem, dlatego niektóre języki oferują wysyłkę statyczną dla określonych metod.

Implementacja C++

C++ używa wczesnego wiązania i oferuje zarówno dynamiczną, jak i statyczną wysyłkę. Domyślna forma wysyłki jest statyczna. Aby uzyskać dynamiczną wysyłkę, programista musi zadeklarować metodę jako wirtualną .

Kompilatory C++ zazwyczaj implementują dynamiczną wysyłkę ze strukturą danych nazywaną tabelą funkcji wirtualnych (vtable), która definiuje mapowanie nazwy do implementacji dla danej klasy jako zestaw wskaźników funkcji składowych. (Jest to czysto szczegółowy opis implementacji; specyfikacja C++ nie wspomina vtables). Instancje tego typu będą następnie przechowywać wskaźnik do tej tabeli jako część swoich danych instancji. Jest to skomplikowane, gdy stosuje się dziedziczenie wielokrotne . Ponieważ C++ nie obsługuje późnego wiązania, tabela wirtualna w obiekcie C++ nie może być modyfikowana w czasie wykonywania, co ogranicza potencjalny zestaw celów wysyłki do skończonego zestawu wybranego w czasie kompilacji.

Przeciążanie typów nie powoduje dynamicznej wysyłki w języku C++, ponieważ język uwzględnia typy parametrów komunikatu będące częścią formalnej nazwy komunikatu. Oznacza to, że nazwa komunikatu, którą widzi programista, nie jest formalną nazwą używaną do wiązania.

Wdrożenie Go and Rust

W Go and Rust zastosowano bardziej wszechstronną odmianę wczesnego wiązania. Wskaźniki Vtable są przenoszone z odniesieniami do obiektów jako „grube wskaźniki” („interfejsy” w Go lub „obiekty cech” w Rust).

To oddziela obsługiwane interfejsy od podstawowych struktur danych. Każda skompilowana biblioteka nie musi znać pełnego zakresu obsługiwanych interfejsów, aby poprawnie używać typu, tylko określony układ vtable, którego wymagają. Kod może przekazywać różne interfejsy do tego samego fragmentu danych do różnych funkcji. Ta wszechstronność odbywa się kosztem dodatkowych danych z każdym odwołaniem do obiektu, co jest problematyczne, jeśli wiele takich odwołań jest przechowywanych w sposób trwały.

Termin gruby wskaźnik odnosi się po prostu do wskaźnika z dodatkowymi powiązanymi informacjami. Dodatkowa informacja może być wskaźnikiem vtable do dynamicznej wysyłki opisanej powyżej, ale częściej jest to rozmiar skojarzonego obiektu do opisania np . slice .

Wdrożenie Smalltalk

Smalltalk używa dyspozytora wiadomości opartego na typie. Każde wystąpienie ma jeden typ, którego definicja zawiera metody. Gdy wystąpienie odbiera komunikat, dyspozytor wyszukuje odpowiednią metodę w mapie komunikat-metoda dla typu, a następnie wywołuje metodę.

Ponieważ typ może mieć łańcuch typów podstawowych, to wyszukiwanie może być kosztowne. Naiwna implementacja mechanizmu Smalltalka wydawałaby się mieć znacznie wyższy narzut niż C++ i ten narzut byłby ponoszony dla każdej wiadomości, którą odbiera obiekt.

Prawdziwe implementacje Smalltalk często używają techniki znanej jako buforowanie inline, która sprawia, że ​​wysyłanie metod jest bardzo szybkie. Buforowanie wbudowane zasadniczo przechowuje poprzedni adres metody docelowej i klasę obiektu witryny wywołania (lub wiele par w przypadku buforowania wielokierunkowego). Buforowana metoda jest inicjowana przy użyciu najbardziej popularnej metody docelowej (lub tylko procedury obsługi błędów pamięci podręcznej), na podstawie selektora metody. Gdy witryna wywołania metody zostanie osiągnięta podczas wykonywania, po prostu wywołuje adres w pamięci podręcznej. (W dynamicznym generatorze kodu to wywołanie jest wywołaniem bezpośrednim, ponieważ adres bezpośredni jest z powrotem poprawiany przez logikę braku pamięci podręcznej). , wykonanie rozgałęzia się do modułu obsługi błędów pamięci podręcznej, aby znaleźć właściwą metodę w klasie. Szybka implementacja może mieć wiele wpisów w pamięci podręcznej i często wystarczy kilka instrukcji, aby uzyskać wykonanie do prawidłowej metody w przypadku początkowego braku pamięci podręcznej. Typowym przypadkiem będzie dopasowanie klasy w pamięci podręcznej, a wykonanie będzie po prostu kontynuowane w metodzie.

Buforowanie poza linią może być również używane w logice wywoływania metody przy użyciu klasy obiektu i selektora metody. W jednym projekcie selektor klasy i metody jest mieszany i używany jako indeks w tablicy pamięci podręcznej wysyłania metod.

Ponieważ Smalltalk jest językiem refleksyjnym, wiele implementacji pozwala na mutowanie pojedynczych obiektów w obiekty za pomocą dynamicznie generowanych tabel wyszukiwania metod. Pozwala to na zmianę zachowania obiektu na podstawie obiektu. Z tego wyrosła cała kategoria języków znanych jako języki oparte na prototypach , z których najbardziej znane to Self i JavaScript . Staranne zaprojektowanie buforowania rozsyłania metod umożliwia nawet językom opartym na prototypach dysponowanie wysoką wydajnością rozsyłania metod.

Wiele innych dynamicznie typowanych języków, w tym Python , Ruby , Objective-C i Groovy, stosuje podobne podejścia.

Przykład w Pythonie

class Cat:
    def speak(self):
        print("Meow")

class Dog:
    def speak(self):
        print("Woof")


def speak(pet):
    # Dynamically dispatches the speak method
    # pet can either be an instance of Cat or Dog
    pet.speak()

cat = Cat()
speak(cat)
dog = Dog()
speak(dog)

Zobacz też

Bibliografia

Bibliografia