stringtranslate.com

Динамическая отправка

В информатике динамическая диспетчеризация — это процесс выбора реализации полиморфной операции ( метода или функции) для вызова во время выполнения . Она обычно используется в языках и системах объектно-ориентированного программирования (ООП) и считается их основной характеристикой . [ 1 ]

Объектно-ориентированные системы моделируют проблему как набор взаимодействующих объектов, которые выполняют операции, ссылающиеся по имени. Полиморфизм — это явление, при котором несколько взаимозаменяемые объекты каждый из них выставляют операцию с тем же именем, но, возможно, отличающуюся поведением. Например, объект File и объект Database имеют метод StoreRecord , который может использоваться для записи записи о персонале в хранилище. Их реализации различаются. Программа содержит ссылку на объект, который может быть либо объектом File , либо объектом Database . Какой именно, может быть определено настройкой времени выполнения, и на этом этапе программа может не знать или не заботиться о том, какой именно. Когда программа вызывает StoreRecord для объекта, что-то должно выбрать, какое поведение будет выполнено. Если представить ООП как отправку сообщений объектам, то в этом примере программа отправляет сообщение StoreRecord объекту неизвестного типа, предоставляя системе поддержки времени выполнения отправку сообщения нужному объекту. Объект выполняет то поведение, которое он реализует. [2]

Динамическая диспетчеризация отличается от статической диспетчеризации , в которой реализация полиморфной операции выбирается во время компиляции . Цель динамической диспетчеризации — отложить выбор подходящей реализации до тех пор, пока не станет известен тип параметра (или нескольких параметров) во время выполнения.

Динамическая диспетчеризация отличается от позднего связывания (также известного как динамическое связывание). Связывание имени связывает имя с операцией. Полиморфная операция имеет несколько реализаций, все из которых связаны с одним и тем же именем. Связывания могут быть сделаны во время компиляции или (с поздним связыванием) во время выполнения. При динамической диспетчеризации одна конкретная реализация операции выбирается во время выполнения. В то время как динамическая диспетчеризация не подразумевает позднее связывание, позднее связывание подразумевает динамическую диспетчеризацию, поскольку реализация операции с поздним связыванием неизвестна до времени выполнения. [ необходима цитата ]

Однократная и многократная отправка

Выбор версии метода для вызова может быть основан либо на одном объекте, либо на комбинации объектов. Первый вариант называется одиночной диспетчеризацией и напрямую поддерживается распространенными объектно-ориентированными языками, такими как Smalltalk , C++ , Java , C# , Objective-C , Swift , JavaScript и Python . В этих и подобных языках можно вызвать метод для деления с синтаксисом, который напоминает

делимое . делить ( делитель )  # делимое / делитель

где параметры необязательны. Это рассматривается как отправка сообщения с именем divide с параметром divisor в dividend . Реализация будет выбрана только на основе типа dividend (возможно, rational , floating point , matrix ), игнорируя тип или значение divisor .

Напротив, некоторые языки диспетчеризируют методы или функции на основе комбинации операндов; в случае деления типы делимого и делителя вместе определяют, какая операция деления будет выполнена. Это известно как множественная диспетчеризация . Примерами языков, которые поддерживают множественную диспетчеризацию, являются Common Lisp , Dylan и Julia .

Динамические механизмы диспетчеризации

Язык может быть реализован с различными механизмами динамической диспетчеризации. Выбор динамического механизма диспетчеризации, предлагаемого языком, в значительной степени изменяет парадигмы программирования, которые доступны или являются наиболее естественными для использования в данном языке.

Обычно в типизированном языке механизм отправки будет выполняться на основе типа аргументов (чаще всего на основе типа получателя сообщения). Языки со слабой или отсутствующей системой типизации часто содержат таблицу отправки как часть данных объекта для каждого объекта. Это позволяет реализовать поведение экземпляра , поскольку каждый экземпляр может сопоставлять данное сообщение с отдельным методом.

Некоторые языки предлагают гибридный подход.

Динамическая диспетчеризация всегда влечет за собой накладные расходы, поэтому некоторые языки предлагают статическую диспетчеризацию для определенных методов.

Реализация на С++

C++ использует раннее связывание и предлагает как динамическую, так и статическую диспетчеризацию. Форма диспетчеризации по умолчанию — статическая. Чтобы получить динамическую диспетчеризацию, программист должен объявить метод как virtual .

Компиляторы C++ обычно реализуют динамическую диспетчеризацию с помощью структуры данных, называемой таблицей виртуальных функций (vtable), которая определяет отображение имени в реализацию для данного класса как набор указателей функций-членов. Это чисто деталь реализации, поскольку спецификация C++ не упоминает vtables. Экземпляры этого типа затем будут хранить указатель на эту таблицу как часть своих данных экземпляра, усложняя сценарии при использовании множественного наследования . Поскольку C++ не поддерживает позднее связывание, виртуальная таблица в объекте C++ не может быть изменена во время выполнения, что ограничивает потенциальный набор целей диспетчеризации конечным набором, выбранным во время компиляции.

Перегрузка типов не приводит к динамической диспетчеризации в C++, поскольку язык считает типы параметров сообщения частью формального имени сообщения. Это означает, что имя сообщения, которое видит программист, не является формальным именем, используемым для связывания.

Реализация Go, Rust и Nim

В Go , Rust и Nim используется более универсальный вариант раннего связывания. Указатели Vtable переносятся вместе со ссылками на объекты как «толстые указатели» («интерфейсы» в Go или «объекты-характеристики» в Rust [3] [4] ).

Это отделяет поддерживаемые интерфейсы от базовых структур данных. Каждая скомпилированная библиотека не должна знать весь спектр поддерживаемых интерфейсов, чтобы правильно использовать тип, а только конкретную схему vtable, которая им требуется. Код может передавать различные интерфейсы к одному и тому же фрагменту данных различным функциям. Эта универсальность достигается за счет дополнительных данных с каждой ссылкой на объект, что проблематично, если много таких ссылок хранятся постоянно.

Термин fat pointer просто относится к указателю с дополнительной связанной информацией. Дополнительная информация может быть указателем vtable для динамической диспетчеризации, описанной выше, но чаще всего это размер связанного объекта для описания, например, среза . [ необходима цитата ]

Реализация Smalltalk

Smalltalk использует диспетчер сообщений на основе типов. Каждый экземпляр имеет один тип, определение которого содержит методы. Когда экземпляр получает сообщение, диспетчер ищет соответствующий метод в карте сообщений-методов для типа, а затем вызывает метод.

Поскольку тип может иметь цепочку базовых типов, этот поиск может быть дорогим. Наивная реализация механизма Smalltalk, по-видимому, имеет значительно более высокие накладные расходы, чем у C++, и эти накладные расходы будут возникать для каждого сообщения, которое получает объект.

Реальные реализации Smalltalk часто используют технику, известную как встроенное кэширование [5] , которая делает отправку методов очень быстрой. Встроенное кэширование в основном сохраняет предыдущий адрес метода назначения и класс объекта места вызова (или несколько пар для многостороннего кэширования). Кэшированный метод инициализируется наиболее распространенным целевым методом (или просто обработчиком промахов кэша) на основе селектора методов. Когда место вызова метода достигается во время выполнения, он просто вызывает адрес в кэше. (В динамическом генераторе кода этот вызов является прямым вызовом, поскольку прямой адрес обратно исправляется логикой промахов кэша.) Затем код пролога в вызываемом методе сравнивает кэшированный класс с фактическим классом объекта, и если они не совпадают, выполнение переходит к обработчику промахов кэша, чтобы найти правильный метод в классе. Быстрая реализация может иметь несколько записей кэша, и часто требуется всего несколько инструкций, чтобы выполнить выполнение правильного метода при начальном промахе кэша. Обычным случаем будет сопоставление кэшированного класса, и выполнение просто продолжится в методе.

Внеочередное кэширование также может использоваться в логике вызова метода, используя класс объекта и селектор метода. В одном из вариантов класс и селектор метода хэшируются и используются как индекс в таблице кэша диспетчеризации метода.

Поскольку Smalltalk является рефлексивным языком, многие реализации позволяют мутировать отдельные объекты в объекты с динамически генерируемыми таблицами поиска методов. Это позволяет изменять поведение объектов на основе каждого объекта. Из этого выросла целая категория языков, известных как языки на основе прототипов , наиболее известные из которых — Self и JavaScript . Тщательная разработка кэширования диспетчеризации методов позволяет даже языкам на основе прототипов иметь высокопроизводительную диспетчеризацию методов.

Многие другие динамически типизированные языки, включая Python , Ruby , Objective-C и Groovy, используют схожие подходы.

Пример на Python

класс  Cat :  def  speak ( self ):  print ( "Мяу" )класс  Собака :  def  speak ( self ):  print ( "Гав" )def  speak ( pet ):  # Динамически отправляет метод speak  # pet может быть экземпляром Cat или Dog  pet . speak ()кот  =  Кот () говорит ( кот ) собака  =  Собака () говорит ( собака )

Пример на C++

#include <iostream> // делаем Pet абстрактным виртуальным базовым классом class Pet { public : virtual void speak () = 0 ; };       class Dog : public Pet { public : void speak () override { std :: cout << "Гав! \n " ; } };             class Cat : public Pet { public : void speak () override { std :: cout << "Мяу! \n " ; } };             // speak() сможет принять все, что происходит от Pet void speak ( Pet & pet ) { pet . speak (); }   int main () { Собака Фидо ; Кот Симба ; говорить ( Фидо ); говорить ( Симба ); вернуть 0 ; }         

Смотрите также

Ссылки

  1. ^ Милтон, Скотт; Шмидт, Хайнц В. (1994). Динамическая диспетчеризация в объектно-ориентированных языках (технический отчет). Том TR-CS-94-02. Австралийский национальный университет. CiteSeerX  10.1.1.33.4292 .
  2. ^ Driesen, Karel; Hölzle, Urs; Vitek, Jan (1995). «Message Dispatch on Pipelined Processors». ECOOP'95 — Object-Oriented Programming, 9th European Conference, Åarhus, Denmark, August 7–11, 1995. Lecture Notes in Computer Science. Vol. 952. Springer. CiteSeerX 10.1.1.122.281 . doi :10.1007/3-540-49538-X_13. ISBN  3-540-49538-X.
  3. ^ Клабник, Стив; Николс, Кэрол (2023) [2018]. "17. Возможности объектно-ориентированного программирования". Язык программирования Rust (2-е изд.). Сан-Франциско, Калифорния, США: No Starch Press, Inc. стр. 375–396 [379–384]. ISBN 978-1-7185-0310-6. стр. 384: Объекты-характеристики выполняют динамическую диспетчеризацию […] Когда мы используем объекты-характеристики, Rust должен использовать динамическую диспетчеризацию. Компилятор не знает всех типов, которые могут использоваться с кодом, использующим объекты-характеристики, поэтому он не знает, какой метод реализован на каком типе для вызова. Вместо этого во время выполнения Rust использует указатели внутри объекта-характеристики, чтобы узнать, какой метод вызывать. Этот поиск влечет за собой затраты времени выполнения, которые не возникают при статической диспетчеризации. Динамическая диспетчеризация также не позволяет компилятору выбирать встраивание кода метода, что, в свою очередь, предотвращает некоторые оптимизации.(xxix+1+527+3 страницы)
  4. ^ "Объекты черт". Справочник Rust . Получено 2023-04-27 .
  5. ^ Мюллер, Мартин (1995). Отправка сообщений в динамически типизированных объектно-ориентированных языках (магистерская диссертация). Университет Нью-Мексико. С. 16–17. CiteSeerX 10.1.1.55.1782 . 

Дальнейшее чтение