stringtranslate.com

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

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

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

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

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

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

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

дивиденды . разделить ( делитель )  # делимое/делитель

где параметры являются необязательными. Это рассматривается как отправка сообщения с именем « divide» с параметром «divisor» в функцию «divided» . Реализация будет выбрана только на основе типа делимого (возможно , рациональное , с плавающей запятой , матрица ), независимо от типа или значения делителя .

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Реализация Smalltalk

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

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

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

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

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

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

Пример на Python

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

Пример на С++

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

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

Рекомендации

  1. ^ Милтон, Скотт; Шмидт, Хайнц В. (1994). Динамическая диспетчеризация в объектно-ориентированных языках (Технический отчет). Том. ТР-КС-94-02. Австралийский национальный университет. CiteSeerX  10.1.1.33.4292 .
  2. ^ Дрисен, Карел; Хёльцле, Урс; Витек, Ян (1995). «Отправка сообщений на конвейерных процессорах». ECOOP'95 — Объектно-ориентированное программирование, 9-я Европейская конференция, Орхус, Дания, 7–11 августа 1995 г. Конспекты лекций по информатике. Том. 952. Спрингер. CiteSeerX 10.1.1.122.281 . дои : 10.1007/3-540-49538-X_13. ISBN  3-540-49538-Х.
  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. ^ «Объекты свойств». Справочник по ржавчине . Проверено 27 апреля 2023 г.
  5. ^ Мюллер, Мартин (1995). Отправка сообщений в динамически типизированных объектно-ориентированных языках (магистерская диссертация). Университет Нью-Мексико. стр. 16–17. CiteSeerX 10.1.1.55.1782 . 

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