Dynamiczna wysyłka

W informatyce dynamiczna wysyłka to proces wybierania, która implementacja operacji polimorficznej ( metody lub funkcji) ma zostać wywołana 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 zestaw oddziałujących ze sobą obiektów, które wykonują operacje, do których odnosi się nazwa. Polimorfizm to zjawisko, w którym każdy z obiektów, które są nieco wymienne, ujawnia operację o tej samej nazwie, ale prawdopodobnie różniącą się zachowaniem. Na przykład File i obiekt Database mają metodę StoreRecord , której można użyć do zapisania akt osobowych w pamięci masowej. Ich implementacje są różne. Program przechowuje odniesienie do obiektu, którym może być File lub obiekt Database . Który to mógł być określony przez ustawienie czasu wykonywania, a na tym etapie program może nie wiedzieć lub nie dbać o to. Kiedy program wywołuje StoreRecord na obiekcie, coś musi wybrać, które zachowanie zostanie wprowadzone. Jeśli ktoś pomyśli, że OOP wysyła komunikaty 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.

Wysyłanie dynamiczne kontrastuje z wysyłaniem statycznym , w którym implementacja operacji polimorficznej jest wybierana w czasie kompilacji . Celem dynamicznego wysyłania jest odroczenie wyboru odpowiedniej implementacji do czasu, aż znany będzie typ czasu wykonywania parametru (lub wielu parametrów).

Wysyłanie dynamiczne różni się od późnego wiązania (znanego również jako wiązanie dynamiczne). Powiązanie nazwy 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 (w przypadku późnego wiązania) w czasie wykonywania. W przypadku dynamicznego wysyłania w czasie wykonywania wybierana jest jedna konkretna implementacja operacji. Podczas gdy dynamiczne wysyłanie nie implikuje późnego wiązania, późne wiązanie implikuje dynamiczną wysyłkę, ponieważ implementacja operacji z późnym wiązaniem nie jest znana do czasu wykonania. [ potrzebne źródło ]

Wysyłka pojedyncza i wielokrotna

Wybór wersji metody do wywołania może opierać się albo na pojedynczym obiekcie, albo na kombinacji obiektów. Ta pierwsza nazywa się pojedynczą wysyłką i jest bezpośrednio obsługiwana przez popularne języki zorientowane obiektowo, takie jak Smalltalk , C++ , Java , C# , Objective-C , Swift , JavaScript i Python . W tych i podobnych językach można nazwać metodę dzielenia o podobnej składni

   dywidenda  .  dzielenie  (  dzielnik  )  # dzielna / dzielnik 

gdzie parametry są opcjonalne. Jest to traktowane jako wysłanie wiadomości o nazwie dziel z parametrem dzielnik do dywidendy . Implementacja zostanie wybrana tylko na podstawie typu dywidendy (być może wymierna , zmiennoprzecinkowa , macierz ), pomijając typ lub wartość dzielnika .

Z kolei niektóre języki wysyłają metody lub funkcje oparte na kombinacji operandów; w przypadku dzielenia typy dzielnej i dzielnika razem określają, która operacja dzielenia zostanie wykonana. Jest to znane jako wysyłka wielokrotna . Przykładami języków obsługujących wielokrotne wysyłanie są Common Lisp , Dylan i Julia .

Dynamiczne mechanizmy wysyłki

Język może być zaimplementowany z różnymi dynamicznymi mechanizmami wysyłania. Wybór mechanizmu dynamicznej wysyłki oferowanego przez język w dużej mierze zmienia 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łania będzie wykonywany na podstawie typu argumentów (najczęściej na podstawie typu odbiorcy komunikatu). Języki ze słabymi systemami pisania lub bez systemów pisania często zawierają tabelę wysyłkową jako część danych opisowych dla każdego obiektu. Pozwala to na zachowanie instancji , ponieważ każda instancja może odwzorować dany komunikat na oddzielną metodę.

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

Wysyłka dynamiczna zawsze będzie wiązać się z narzutem, dlatego niektóre języki oferują statyczną wysyłkę 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 virtual .

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

Przeciążanie typów nie powoduje dynamicznej wysyłki w C++, ponieważ język traktuje typy parametrów wiadomości jako część formalnej nazwy wiadomości. Oznacza to, że nazwa komunikatu widziana przez programistę nie jest formalną nazwą używaną do wiązania.

Implementacja Go i Rust

W Go , Rust i Nim używana jest bardziej wszechstronna odmiana 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). [ potrzebne źródło ]

To oddziela obsługiwane interfejsy od bazowych struktur danych. Każda skompilowana biblioteka nie musi znać pełnego zakresu obsługiwanych interfejsów, aby poprawnie używać typu, a jedynie 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 odniesień jest przechowywanych w sposób trwały.

Termin gruby wskaźnik odnosi się po prostu do wskaźnika z dodatkowymi powiązanymi informacjami. Dodatkową informacją może być wskaźnik vtable dla dynamicznej wysyłki opisanej powyżej, ale częściej jest to rozmiar powiązanego obiektu opisujący np. wycinek . [ potrzebne źródło ]

Implementacja Smalltalka

Smalltalk używa dyspozytora wiadomości opartego na typie. Każda instancja ma jeden typ, którego definicja zawiera metody. Gdy instancja odbiera komunikat, dyspozytor wyszukuje odpowiednią metodę na 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 Smalltalk wydaje się mieć znacznie wyższy narzut niż w C++, a ten narzut byłby ponoszony dla każdej wiadomości, którą otrzymuje obiekt.

Implementacje Real Smalltalk często wykorzystują technikę zwaną buforowaniem wbudowanym , która sprawia, że ​​wysyłanie metod jest bardzo szybkie. Buforowanie wbudowane zasadniczo przechowuje poprzedni adres metody docelowej i klasę obiektu witryny wywołującej (lub wiele par w przypadku buforowania wielokierunkowego). Metoda buforowana jest inicjowana przy użyciu najczęstszej metody docelowej (lub tylko procedury obsługi chybień w pamięci podręcznej) na podstawie selektora metody. Gdy strona wywołania metody zostanie osiągnięta podczas wykonywania, po prostu wywołuje adres w pamięci podręcznej. (W generatorze kodu dynamicznego to wywołanie jest wywołaniem bezpośrednim, ponieważ bezpośredni adres jest ponownie poprawiany przez logikę braku pamięci podręcznej). Kod prologu w wywoływanej metodzie porównuje następnie klasę buforowaną z rzeczywistą klasą obiektu, a jeśli nie pasują , wykonanie rozgałęzia się do modułu obsługi chybień w pamięci podręcznej, aby znaleźć poprawną metodę w klasie. Szybka implementacja może mieć wiele wpisów w pamięci podręcznej i często wystarczy kilka instrukcji, aby wykonać właściwą metodę przy początkowym chybieniu w 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 wierszem może być również używane w logice wywołania metody, przy użyciu klasy obiektu i selektora metody. W jednym projekcie selektor klasy i metody jest mieszany i używany jako indeks w tabeli pamięci podręcznej rozsyłania metod.

Ponieważ Smalltalk jest językiem refleksyjnym, wiele implementacji umożliwia mutowanie pojedynczych obiektów w obiekty za pomocą dynamicznie generowanych tabel wyszukiwania metod. Pozwala to na zmianę zachowania obiektu dla każdego 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 wysyłania metod pozwala nawet językom opartym na prototypach na wysokowydajne wysyłanie metod.

Wiele innych języków z dynamicznym typowaniem, w tym Python , Ruby , Objective-C i Groovy , stosuje podobne podejście.

Przykład w Pythonie

 
     
        

 
     
        


 
    
    
    

  

  
 class  Cat  :  def  speak  (  self  ):  print  (  „Miau”  )  class  Dog  :  def  speak  (  self  ):  print  (  „Woof”  )  def  speak  (  pet  ):  # Dynamicznie wywołuje metodę speak  # pet może być instancją Kot lub pies  zwierzak  .  mów  ()  kot  =  kot  ()  mów  (  kot  )  pies  =  pies  ()  mów  (  pies  ) 

Przykład w C++

 


  

        0


     

      
    
          
    


     

      
    
          
    



  

    


 

     
     
    
    
     0
 #include  <iostream>  // uczyń Pet abstrakcyjną wirtualną klasą bazową  class  Pet  {  public  :  virtual  void  speak  ()  =  ;  };  klasa  Pies  :  public  Pet  {  public  :  void  speak  ()  override  {  std  ::  cout  <<  "Hau!  \n  "  ;  }  };  class  Kot  :  public  Pet  {  public  :  void  speak  ()  override  {  std  ::  cout  <<  "Miau!  \n  "  ;  }  };  // speak() będzie w stanie zaakceptować wszystko, co pochodzi od Pet  void  speak  (  Pet  &  pet  )  {  pet  .  mówić  ();  }  int  main  ()  {  Dog  fido  ;  kot  simba  ;  mówić  (  fido  );  mówić  (  simba  );  powrót  ;  } 

Zobacz też

Dalsza lektura

  •   Lippman, Stanley B. (1996). Wewnątrz modelu obiektowego C++ . Addison-Wesley . ISBN 0-201-83454-5 .
  • Groeber, Marcus; Di Geronimo, Jr., Edward „Ed”; Paweł, Matthias R. (2002-03-02) [2002-02-24]. „Informacje GEOS/NDO dla RBIL62?” . Grupa dyskusyjna : comp.os.geos.programmer . Zarchiwizowane od oryginału w dniu 2019-04-20 . Źródło 2019-04-20 . […] Powodem, dla którego Geos potrzebuje 16 przerwań, jest to, że schemat służy do konwersji wywołań funkcji międzysegmentowych („dalekich”) na przerwania bez zmiany rozmiaru kodu. Powodem tego jest to, że „coś” (jądro) może zaczepić się o każde wywołanie międzysegmentowe wykonane przez aplikację Geos i upewnić się, że odpowiednie segmenty kodu są ładowane z pamięci wirtualnej i blokowane. W DOS byłoby to porównywalne z programem ładującym nakładki , ale takim, który można dodać bez konieczności wyraźnego wsparcia ze strony kompilatora lub aplikacji. Dzieje się to mniej więcej tak: […] 1. Kompilator trybu rzeczywistego generuje następującą instrukcję: CALL <segment>:<offset> -> 9A <offlow><offhigh><seglow><seghigh> z <seglow>< seghigh> jest zwykle definiowany jako adres, który musi zostać naprawiony w czasie ładowania w zależności od adresu, pod którym został umieszczony kod. […] 2. Konsolidator Geos zamienia to na coś innego: INT 8xh -> CD 8x […] DB <seghigh>,<offlow>,<offhigh> […] Zauważ, że to znowu pięć bajtów, więc może być ustawiony „na miejscu”. Teraz problem polega na tym, że przerwanie wymaga dwóch bajtów, podczas gdy instrukcja CALL FAR potrzebuje tylko jednego. W rezultacie 32-bitowy wektor (<seg><ofs>) musi zostać skompresowany do 24 bitów. […] Osiąga się to za pomocą dwóch rzeczy: Po pierwsze, adres <seg> jest zakodowany jako „uchwyt” segmentu, którego najniższy półbajt jest zawsze równy zero. Pozwala to zaoszczędzić cztery bity. Ponadto […] pozostałe cztery bity przechodzą do niskiego półbajtu wektora przerwań, tworząc w ten sposób wszystko od INT 80h do 8Fh. […] Program obsługi przerwań dla wszystkich tych wektorów jest taki sam. „Rozpakuje” adres z trzyipółbajtowej notacji, wyszuka adres bezwzględny segmentu i przekaże wywołanie po zakończeniu ładowania pamięci wirtualnej… Powrót z wywołania również przejść przez odpowiedni kod odblokowujący. […] Niski półbajt wektora przerwań (80h – 8Fh) przechowuje bity od 4 do 7 uchwytu segmentu. Bity od 0 do 3 uchwytu segmentu są (z definicji uchwytu Geos) zawsze równe 0. […] wszystkie interfejsy API Geos działają według schematu „nakładki” […]: kiedy aplikacja Geos jest ładowana do pamięci, program ładujący automatycznie zastąpić wywołania funkcji w bibliotekach systemowych odpowiednimi wywołaniami opartymi na INT. W każdym razie nie są one stałe, ale zależą od uchwytu przypisanego do segmentu kodu biblioteki. […] Geos pierwotnie miał zostać przekonwertowany do trybu chronionego bardzo wcześnie […], przy czym tryb rzeczywisty był tylko „starszą opcją” […] prawie każda linia kodu asemblera jest na to gotowa […]
  • Paweł, Matthias R. (2002-04-11). „Re: [fd-dev] OGŁOSZENIE: CuteMouse 2.0 alfa 1” . freedos-dev . Zarchiwizowane od oryginału w dniu 2020-02-21 . Źródło 2020-02-21 . […] w przypadku takich zniekształconych wskaźników […] wiele lat temu Axel i ja zastanawialiśmy się, jak wykorzystać *jeden* punkt wejścia do sterownika dla wielu wektorów przerwań (ponieważ zaoszczędziłoby nam to dużo miejsca na wiele punktów wejścia i mniej więcej identyczny kod ramki startowej/wyjściowej we wszystkich), a następnie wewnętrznie przełącz się na różne procedury obsługi przerwań. Na przykład: 1234h:0000h […] 1233h:0010h […] 1232h:0020h […] 1231h:0030h […] 1230h:0040h […] wszystkie wskazują dokładnie ten sam punkt wejścia. Jeśli zaczepisz INT 21h na 1234h:0000h i INT 2Fh na 1233h:0010h i tak dalej, wszystkie przejdą przez tę samą „lukę”, ale nadal będziesz w stanie rozróżnić je i wewnętrznie rozgałęzić się na różne procedury obsługi. Pomyśl o „skompresowanym” punkcie wejścia do A20 do ładowania HMA . Działa to tak długo, jak żaden program nie zaczyna wykonywać magii segmentu: przesunięcia. … ] Porównaj to z odwrotnym podejściem, aby mieć wiele punktów wejścia (być może nawet obsługujących protokół IBM Interrupt Sharing Protocol ), który zużywa znacznie więcej pamięci, jeśli przechwytujesz wiele przerwań. […] Doszliśmy do wniosku, że w praktyce najprawdopodobniej nie byłoby to bezpieczne, ponieważ nigdy nie wiadomo, czy inne sterowniki normalizują lub denormalizują wskaźniki, z jakich powodów. […] (Uwaga. Coś podobnego do „ grubych wskaźników ” specjalnie dla segmentu trybu rzeczywistego Intela : adresowanie z przesunięciem w procesorach x86 , zawierające zarówno celowo nieznormalizowany wskaźnik do wspólnego punktu wejścia kodu, jak i pewne informacje, aby nadal rozróżniać różne wywołujące Podczas gdy w systemie otwartym nie można całkowicie wykluczyć normalizacji wskaźników zewnętrznych instancji (w innych sterownikach lub aplikacjach) na interfejsach publicznych , schematu można bezpiecznie używać na interfejsach wewnętrznych, aby uniknąć zbędnych sekwencji kodów wejściowych .)
  • Jasne, Walter (22.12.2009). „Największy błąd C” . Cyfrowy Mars . Zarchiwizowane od oryginału w dniu 2022-06-08 . Źródło 2022-07-11 . [1]
  • Holden, Daniel (2015). „Biblioteka grubych wskaźników” . Wiolonczela: wysoki poziom C. Zarchiwizowane od oryginału w dniu 2022-07-11 . Źródło 2022-07-11 .