Множественная диспетчеризация или мультиметоды — это функция некоторых языков программирования , в которой функция или метод могут быть динамически диспетчеризированы на основе типа времени выполнения (динамического) или, в более общем случае, некоторого другого атрибута более чем одного из ее аргументов . [1] Это обобщение полиморфизма одиночной диспетчеризации , где вызов функции или метода динамически диспетчеризируется на основе производного типа объекта, для которого был вызван метод. Множественная диспетчеризация направляет динамическую диспетчеризацию к реализующей функции или методу, используя объединенные характеристики одного или нескольких аргументов.
Разработчики компьютерного программного обеспечения обычно организуют исходный код в именованные блоки, которые по-разному называются подпрограммами , процедурами, подпрограммами, функциями или методами. Код в функции выполняется путем ее вызова — выполнения фрагмента кода, который ссылается на ее имя . Это временно передает управление вызываемой функции; когда выполнение функции завершается, управление обычно передается обратно инструкции в вызывающем объекте , которая следует за ссылкой.
Имена функций обычно выбираются так, чтобы они описывали назначение функции. Иногда желательно дать нескольким функциям одно и то же имя, часто потому, что они выполняют концептуально схожие задачи, но работают с разными типами входных данных. В таких случаях ссылка на имя в месте вызова функции недостаточна для идентификации блока кода, который должен быть выполнен. Вместо этого для выбора среди нескольких реализаций функции также используются количество и тип аргументов вызова функции.
В более традиционных, т. е. однодиспетчерских объектно-ориентированных языках программирования, при вызове метода ( отправка сообщения в Smalltalk , вызов функции-члена в C++ ) один из его аргументов обрабатывается особым образом и используется для определения того, какой из (потенциально многих) классов методов с таким именем должен быть применен. Во многих языках специальный аргумент указывается синтаксически; например, ряд языков программирования помещают специальный аргумент перед точкой при вызове метода: special.method(other, arguments, here)
, так что это lion.sound()
вызовет рев, тогда как sparrow.sound()
вызовет щебетание.
Напротив, в языках с множественной диспетчеризацией выбранный метод — это просто тот, аргументы которого соответствуют номеру и типу вызова функции. Не существует специального аргумента, который владеет функцией/методом, выполняемым в конкретном вызове.
Common Lisp Object System (CLOS) — ранний и известный пример множественной диспетчеризации. Другим примечательным примером использования множественной диспетчеризации является язык программирования Julia .
Множественную диспетчеризацию следует отличать от перегрузки функций , в которой статическая типизированная информация, такая как объявленный или выведенный тип термина (или базовый тип в языке с подтипированием), используется для определения того, какая из нескольких возможностей будет использоваться в данном месте вызова, и это определение делается во время компиляции или компоновки (или в какое-то другое время до начала выполнения программы) и впоследствии является инвариантным для данного развертывания или запуска программы. Многие языки, такие как C++, предлагают надежную перегрузку функций, но не предлагают динамическую множественную диспетчеризацию (C++ допускает только динамическую одиночную диспетчеризацию посредством использования виртуальных функций).
При работе с языками, которые могут различать типы данных во время компиляции , выбор среди альтернатив может произойти тогда. Акт создания таких альтернативных функций для выбора во время компиляции обычно называется перегрузкой функции.
В языках программирования, которые откладывают идентификацию типа данных до времени выполнения (т. е. позднее связывание ), выбор среди альтернативных функций должен происходить тогда, на основе динамически определяемых типов аргументов функции. Функции, альтернативные реализации которых выбираются таким образом, обычно называются мультиметодами .
Существуют некоторые затраты времени выполнения, связанные с динамической диспетчеризацией вызовов функций. В некоторых языках [ требуется цитата ] различие между перегрузкой и мультиметодами может быть размыто, поскольку компилятор определяет, можно ли применить выбор времени компиляции к данному вызову функции или требуется более медленная диспетчеризация времени выполнения.
Существует несколько известных проблем с динамической диспетчеризацией, как одиночной, так и множественной. Хотя многие из этих проблем решены для одиночной диспетчеризации, которая десятилетиями была стандартной функцией объектно-ориентированных языков программирования, эти проблемы становятся более сложными в случае множественной диспетчеризации.
В большинстве популярных языков программирования исходный код поставляется и развертывается в гранулах функциональности, которые мы здесь будем называть пакетами ; фактическая терминология для этой концепции различается в зависимости от языка. Каждый пакет может содержать несколько определений типов, значений и функций, пакеты часто компилируются отдельно в языках с шагом компиляции, и может существовать нециклическая зависимость. Полная программа представляет собой набор пакетов с основным пакетом , который может зависеть от нескольких других пакетов, и вся программа состоит из транзитивного замыкания зависимости.
Так называемая проблема выражения относится к способности кода в зависимом пакете расширять поведение (функции или типы данных), определенное в базовом пакете из включающего пакета, без изменения исходного кода в базовом пакете. Традиционные ОО-языки с одиночной диспетчеризацией делают добавление новых типов данных, но не новых функций, тривиальным; традиционные функциональные языки, как правило, имеют противоположный эффект, а множественная диспетчеризация, если реализована правильно, допускает оба варианта. Желательно, чтобы реализация множественной диспетчеризации имела следующие свойства:
Обычно желательно, чтобы для любого данного вызова мультиметода был не более одного «лучшего» кандидата среди случаев реализации мультиметода, и/или чтобы, если его нет, это было решено предсказуемым и детерминированным образом, включая неудачу. Недетерминированное поведение нежелательно. Предполагая набор типов с нециклическим отношением подтипирования, можно определить, что одна реализация мультиметода «лучше» (более конкретна), если все динамически-отправляемые аргументы в первом являются подтипами всех динамически-отправляемых аргументов, указанных во втором, и по крайней мере один является строгим подтипом. При одиночной диспетчеризации и в отсутствие множественного наследования это условие выполняется тривиально, но при множественной диспетчеризации возможно, что два или более кандидатов удовлетворят заданному фактическому списку аргументов, но ни один из них не будет более конкретен, чем другой (один динамический аргумент является подтипом в одном случае, другой является подтипом в другом случае). Это, в частности, может произойти, если два разных пакета, не зависящие друг от друга, расширяют некоторый мультиметод с реализациями, касающимися типов каждого пакета, а затем третий пакет, который включает оба (возможно, косвенно), затем вызывает мультиметод, используя аргументы из обоих пакетов.
Возможные решения включают в себя:
Эффективная реализация одиночной диспетчеризации, в том числе в языках программирования, которые отдельно компилируются в объектный код и связываются с низкоуровневым (не учитывающим язык) компоновщиком, в том числе динамически во время загрузки/запуска программы или даже под руководством кода приложения, хорошо известна. Метод " vtable ", разработанный в C++ и других ранних ОО-языках (где каждый класс имеет массив указателей функций, соответствующих виртуальным функциям этого класса), почти так же быстр, как вызов статического метода, требуя накладных расходов O(1) и только одного дополнительного поиска в памяти даже в неоптимизированном случае. Однако метод vtable использует имя функции, а не тип аргумента в качестве ключа поиска и не масштабируется до случая множественной диспетчеризации. (Это также зависит от объектно-ориентированной парадигмы методов, являющихся функциями классов, а не автономными сущностями, независимыми от какого-либо конкретного типа данных).
Эффективная реализация множественной диспетчеризации остается актуальной исследовательской проблемой.
Чтобы оценить, как часто множественная диспетчеризация используется на практике, Мушевичи и др. [2] изучили программы, которые используют динамическую диспетчеризацию. Они проанализировали девять приложений, в основном компиляторов, написанных на шести разных языках: Common Lisp Object System , Dylan , Cecil , MultiJava, Diesel и Nice. Их результаты показывают, что 13–32% универсальных функций используют динамический тип одного аргумента, в то время как 2,7–6,5% из них используют динамический тип нескольких аргументов. Остальные 65–93% универсальных функций имеют один конкретный метод (переопределение) и, таким образом, не считаются использующими динамические типы своих аргументов. Кроме того, исследование сообщает, что 2–20% универсальных функций имели две и 3–6% имели три конкретные реализации функций. Цифры быстро уменьшаются для функций с более конкретными переопределениями.
Множественная диспетчеризация используется гораздо активнее в Julia , где множественная диспетчеризация была центральной концепцией дизайна с самого начала языка: собрав ту же статистику, что и Мушевичи, по среднему количеству методов на универсальную функцию, было обнаружено, что стандартная библиотека Julia использует более чем в два раза больше перегрузки, чем в других языках, проанализированных Мушевичи, и более чем в 10 раз в случае бинарных операторов . [3]
Данные из этих статей обобщены в следующей таблице, где коэффициент отправки DR
— это среднее число методов на общую функцию; коэффициент выбора CR
— это среднее квадрата числа методов (чтобы лучше измерить частоту функций с большим числом методов); [2] [3] а степень специализации DoS
— это среднее число аргументов, специализированных по типу, на метод (т. е. число аргументов, которые отправляются):
Теория множественных языков диспетчеризации была впервые разработана Кастаньей и др. путем определения модели для перегруженных функций с поздним связыванием . [4] [5] Она дала первую формализацию проблемы ковариантности и контравариантности объектно-ориентированных языков [6] и решение проблемы бинарных методов. [7]
Различение множественной и одиночной отправки может быть более понятным на примере. Представьте себе игру, в которой среди (видимых пользователю) объектов есть космические корабли и астероиды. Когда два объекта сталкиваются, программе может потребоваться выполнить разные действия в зависимости от того, что только что с чем столкнулось.
C# представил поддержку динамических мультиметодов в версии 4 [8] (апрель 2010 г.) с использованием ключевого слова «dynamic». Следующий пример демонстрирует мультиметоды. Как и многие другие статически типизированные языки, C# также поддерживает статическую перегрузку методов. [9] Microsoft ожидает, что разработчики выберут статическую типизацию вместо динамической в большинстве сценариев. [10] Ключевое слово «dynamic» поддерживает взаимодействие с объектами COM и динамически типизированными языками .NET.
В приведенном ниже примере используются функции, представленные в C# 9 и C# 10.
с использованием статической ColliderLibrary ; Console.WriteLine ( Collide ( new Asteroid ( 101 ) , new Spaceship ( 300 ))); Console.WriteLine ( Collide ( new Asteroid ( 10 ) , new Spaceship ( 10 ) )); Console.WriteLine ( Collide ( new Spaceship ( 101 ) , new Spaceship ( 10 ) ) ); string Collide ( SpaceObject x , SpaceObject y ) => x . Size > 100 && y . Size > 100 ? "Большой бум!" : CollideWith ( x as dynamic , y as dynamic ); // Динамическая отправка в метод CollideWith class ColliderLibrary { public static string CollideWith ( Астероид x , Астероид y ) => "a/a" ; public static string CollideWith ( Астероид x , Космический корабль y ) => "a/s" ; public static string CollideWith ( Космический корабль x , Астероид y ) => "s/a" ; public static string CollideWith ( Космический корабль x , Космический корабль y ) => "s/s" ; } абстрактная запись SpaceObject ( int Size ); запись Asteroid ( int Size ) : SpaceObject ( Size ); запись Spaceship ( int Size ) : SpaceObject ( Size );
Выход:
Большой бум! а/с с/с
Groovy — это универсальный язык JVM , совместимый с Java и совместимый с ней , который, в отличие от Java, использует позднее связывание и множественную диспетчеризацию. [11]
/* Groovy-реализация примера C# выше Позднее связывание работает одинаково при использовании нестатических методов или статической компиляции класса/методов (аннотация @CompileStatic) */ 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 ) // Динамическая отправка в метод collideWith } частная статическая строка collideWith ( Астероид x , Астероид y ) { "a/a" } частная статическая строка collideWith ( Астероид x , Космический корабль y ) { "a/s" } частная статическая строка collideWith ( Космический корабль x , Астероид y ) { "s/a" } частная статическая строка collideWith ( Космический корабль x , Космический корабль y ) { "s/s" } } класс SpaceObject { int size SpaceObject ( int size ) { this . size = size } } @InheritConstructors класс Asteroid расширяет SpaceObject {} @InheritConstructors класс Spaceship расширяет SpaceObject {}
В языке с множественной диспетчеризацией, таком как Common Lisp , это может выглядеть примерно так (показан пример Common Lisp):
( defmethod collide-with (( x asteroid ) ( y asteroid )) ;; справиться с астероидом, врезающимся в астероид ) ( defmethod collide-with (( x asteroid ) ( y spaceship )) ;; справиться с астероидом, врезающимся в космический корабль ) ( defmethod collide-with (( x spaceship ) ( y asteroid )) ;; справиться с космическим кораблем, врезающимся в астероид ) ( defmethod collide-with (( x spaceship ) ( y spaceship )) ;; справиться с космическим кораблем, врезающимся в космический корабль )
и аналогично для других методов. Явное тестирование и «динамическое приведение» не используются.
При наличии множественной диспетчеризации традиционная идея методов, как определяемых в классах и содержащихся в объектах, становится менее привлекательной — каждый метод collide-with выше прикреплен к двум разным классам, а не к одному. Следовательно, специальный синтаксис для вызова метода, как правило, исчезает, так что вызов метода выглядит точно так же, как и обычный вызов функции, а методы группируются не в классах, а в универсальных функциях .
Julia имеет встроенную множественную диспетчеризацию, и она является центральной в дизайне языка. [3] Версия Julia примера выше может выглядеть так:
абстрактный тип SpaceObject конец struct Asteroid <: SpaceObject размер :: Int конец struct Spaceship <: SpaceObject размер :: Int конец collide_with ( :: Астероид , :: Космический корабль ) = "a/s" collide_with ( :: Космический корабль , :: Астероид ) = "s/a" collide_with ( :: Космический корабль , :: Космический корабль ) = "s/s" collide_with ( :: Астероид , :: Астероид ) = "a/a" collide ( x :: SpaceObject , y :: SpaceObject ) = ( x.size > 100 && y.size > 100 ) ? " Большой бум ! " : collide_with ( x , y )
Выход:
julia> столкновение ( Астероид ( 101 ), Космический корабль ( 300 )) "Большой бум!" julia> столкновение ( Астероид ( 10 ), Космический корабль ( 10 )) "a/s" julia> столкновение ( Космический корабль ( 101 ), Космический корабль ( 10 )) "с/с"
Raku , как и Perl, использует проверенные идеи из других языков, а системы типов показали себя как обеспечивающие убедительные преимущества при анализе кода на стороне компилятора и мощную семантику на стороне пользователя посредством множественной диспетчеризации.
Он имеет как мультиметоды, так и мультиподы. Поскольку большинство операторов являются подпрограммами, он также имеет несколько отправленных операторов.
Наряду с обычными ограничениями типа, он также имеет ограничения where , которые позволяют создавать очень специализированные подпрограммы.
подмножество Масса Действительного , где 0 ^..^ Inf ; роль Звездный-Объект { имеет Массу $.масса обязательна ; имя метода ( ) возвращает Str {...}; }класс Астероид делает Stellar-Object { имя метода () { 'астероид' }}класс Spaceship делает Stellar-Object { имеет Str $.name = 'какой-то безымянный космический корабль' ;}my Str @destroyed = < уничтожен разрушен изуродован >; my Str @damaged = « поврежден 'столкнулся с' 'был поврежден' »;# Мы добавляем несколько кандидатов к числовым операторам сравнения, потому что мы сравниваем их численно, # но нет смысла приводить объекты к числовому типу. # (Если бы они приводились, нам не обязательно было бы добавлять эти операторы. ) # Мы могли бы также определить совершенно новые операторы таким же образом. 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 . масса } мульти суб инфикс: « == » ( Звездный-Объект:D $a , Звездный-Объект:D $b ) { $a . масса == $b . масса }# Определить новый мультидиспетчер и добавить некоторые ограничения типа к параметрам. # Если бы мы его не определили, то получили бы универсальный, не имеющий ограничений. proto sub collide ( Stellar-Object:D $, Stellar-Object:D $ ) {*}# Нет необходимости повторять типы здесь, поскольку они такие же, как у прототипа. # Ограничение 'where' технически применяется только к $b, а не ко всей сигнатуре. # Обратите внимание, что ограничение 'where' использует кандидат на оператор `<`, который мы добавили ранее. 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 ) { # переадресация предыдущему кандидату с аргументами, поменянными местами samewith $b , $a ;}# Это должно быть после первых двух, потому что у других есть ограничения 'where', которые проверяются в том # порядке, в котором были написаны подпрограммы. (Эта подпрограмма всегда будет соответствовать.) multi sub collide ( $a , $b ) { # рандомизировать порядок my ( $n1 , $n2 ) = ( $a . name , $b . name ). pick (*); say "$n1 @damaged.pick() $n2" ;}# Следующие два кандидата могут быть где угодно после прото, # поскольку они имеют более специализированные типы, чем предыдущие три.# Если массы кораблей не равны, то вместо них вызывается один из первых двух кандидатов. multi sub collide ( Spaceship $a , Spaceship $b where $a == $b ){ my ( $n1 , $n2 ) = ( $a . name , $b . name ). pick (*); say "$n1 столкнулся с $n2, и оба корабля были " , ( @destroyed . pick , 'оставлен поврежденным' ). pick ;}# Вы можете распаковать атрибуты в переменные внутри сигнатуры. # Вы даже можете иметь ограничение на них `(:mass($a) where 10)`. multi sub collide ( Asteroid $ (: mass ( $a )), Asteroid $ (: mass ( $b )) ){ say "два астероида столкнулись и объединились в один больший астероид массой { $a + $b }" ;}мой космический корабль $Enterprise .= new (: mass ( 1 ),: name ( 'The Enterprise' )); столкновение Астероид . new (: mass ( .1 )), $Enterprise ; столкновение $Enterprise , Космический корабль . new ( : mass ( .1 )); столкновение $Enterprise , Астероид . new (: mass ( 1 )); столкновение $Enterprise , Космический корабль . new (: mass ( 1 )); столкновение Астероид . new (: mass ( 10 )), Астероид . new (: mass ( 5 ));
В языках, которые не поддерживают множественную диспетчеризацию на уровне определения языка или синтаксиса, часто можно добавить множественную диспетчеризацию с помощью расширения библиотеки . JavaScript и TypeScript не поддерживают мультиметоды на уровне синтаксиса, но можно добавить множественную диспетчеризацию с помощью библиотеки. Например, пакет multimethod [12] предоставляет реализацию множественной диспетчеризации, универсальных функций.
Версия с динамической типизацией в JavaScript:
импорт { multi , method } из '@arrows/multimethod' класс Астероид {} класс Космический корабль {} const collideWith = multi ( method ([ Asteroid , Asteroid ], ( x , y ) => { // справиться с астероидом, сталкивающимся с астероидом }), method ([ Asteroid , Spaceship ], ( x , y ) => { // справиться с астероидом, сталкивающимся с космическим кораблем }), method ([ Spaceship , Asteroid ], ( x , y ) => { // справиться с космическим кораблем, сталкивающимся с астероидом }), method ([ Spaceship , Spaceship ], ( x , y ) => { // справиться с космическим кораблем, сталкивающимся с космическим кораблем }), )
Статически типизированная версия на TypeScript:
импорт { multi , method , Multi } из '@arrows/multimethod' класс Астероид {} класс Космический корабль {} тип CollideWith = Multi & { ( x : Астероид , y : Астероид ) : void ( x : Астероид , y : Космический корабль ) : void ( x : Космический корабль , y : Астероид ) : void ( x : Космический корабль , y : Космический корабль ) : void } const collideWith : CollideWith = multi ( method ([ Asteroid , Asteroid ], ( x , y ) => { // справиться с астероидом, сталкивающимся с астероидом }), method ([ Asteroid , Spaceship ], ( x , y ) => { // справиться с астероидом, сталкивающимся с космическим кораблем }), method ([ Spaceship , Asteroid ], ( x , y ) => { // справиться с космическим кораблем, сталкивающимся с астероидом }), method ([ Spaceship , Spaceship ], ( x , y ) => { // справиться с космическим кораблем, сталкивающимся с космическим кораблем }), )
Множественную диспетчеризацию можно добавить в Python с помощью расширения библиотеки . Например, с помощью модуля multimethod.py [13] , а также с помощью модуля multimethods.py [14] , который предоставляет мультиметоды в стиле CLOS для Python без изменения базового синтаксиса или ключевых слов языка.
из multimethods import Dispatch из game_objects import Asteroid , Spaceship из game_behaviors import as_func , ss_func , sa_funccollide = Dispatch () collide.add_rule (( Астероид , Космический корабль ) , as_func ) collide.add_rule (( Космический корабль , Космический корабль ) , ss_func ) collide.add_rule ( ( Космический корабль , Астероид ) , sa_func ) def aa_func ( a , b ): """Поведение при столкновении астероида с астероидом.""" # ...определить новое поведение... collide.add_rule ( ( Астероид , Астероид ) , aa_func )
# ...позже... столкновение ( вещь1 , вещь2 )
Функционально это очень похоже на пример CLOS, но синтаксис — обычный для Python.
Используя декораторы Python 2.4 , Гвидо ван Россум создал пример реализации мультиметодов [15] с упрощенным синтаксисом:
@multimethod ( Asteroid , Asteroid ) def collide ( a , b ): """Поведение при столкновении астероида с астероидом.""" # ...определить новое поведение... @multimethod ( Asteroid , Spaceship ) def collide ( a , b ): """Поведение при столкновении астероида с космическим кораблем.""" # ...определить новое поведение... # ...определить другие правила мультиметода ...
а затем переходит к определению многометодного декоратора.
Пакет PEAK-Rules обеспечивает множественную диспетчеризацию с синтаксисом, похожим на приведенный выше пример. [16] Позднее он был заменен PyProtocols. [17]
Библиотека Reg также поддерживает множественную и предикатную диспетчеризацию. [18]
С введением подсказок типа возможна множественная диспетчеризация с еще более простым синтаксисом. Например, с помощью plum-dispatch,
от отправки импорта сливы @dispatch def collide ( a : Asteroid , b : Asteroid ): """Поведение при столкновении астероида с астероидом.""" # ...определить новое поведение... @dispatch def collide ( a : Asteroid , b : Spaceship ): """Поведение при столкновении астероида с космическим кораблем.""" # ...определить новое поведение... # ...определить дальнейшие правила...
В языке C нет динамической диспетчеризации, поэтому ее необходимо реализовать вручную в какой-либо форме. Часто для идентификации подтипа объекта используется перечисление. Динамическая диспетчеризация может быть выполнена путем поиска этого значения в таблице ветвления указателя функции . Вот простой пример на языке C:
typedef void ( * CollisionCase )( void ); void collision_AA ( void ) { /* обработка столкновения астероида с астероидом */ }; void collision_AS ( void ) { /* обработка столкновения астероида с космическим кораблем */ }; void collision_SA ( void ) { /* обработка столкновения космического корабля с астероидом */ }; void collision_SS ( void ) { / * обработка столкновения космического корабля с космическим кораблем*/ }; typedef enum { THING_ASTEROID = 0 , THING_SPACESHIP , THING_COUNT /* сам по себе не является типом вещи, а используется для поиска количества вещей */ } Thing ; CollisionCase collisionCases [ THING_COUNT ][ THING_COUNT ] = { { & collision_AA , & collision_AS }, { & collision_SA , & collision_SS } }; void collide ( Вещь a , Вещь b ) { ( * collisionCases [ a ][ b ])(); } int main ( void ) { collide ( ВЕЩЬ_КОСМИЧЕСКИЙ_КОРАБЛЬ , ВЕЩЬ_АСТЕРОИД ); }
С библиотекой C Object System [19] C поддерживает динамическую диспетчеризацию, похожую на CLOS. Она полностью расширяема и не требует ручной обработки методов. Динамические сообщения (методы) отправляются диспетчером COS, который быстрее Objective-C. Вот пример в COS:
#include <stdio.h> #include <cos/Object.h> #include <cos/gen/object.h> // классыdefclass ( Asteroid ) // элементы данных endclass defclass ( Космический корабль ) // элементы данных endclass // дженерикиdefgeneric ( _Bool , collide_with , _1 , _2 ); // мультиметодыdefmethod ( _Bool , collide_with , Asteroid , Asteroid ) // справиться с астероидом, сталкивающимся с астероидом endmethod defmethod ( _Bool , collide_with , Asteroid , Spaceship ) // справиться с астероидом, сталкивающимся с космическим кораблем endmethod defmethod ( _Bool , collide_with , Spaceship , Asteroid ) // справиться с космическим кораблем, сталкивающимся с астероидом endmethod defmethod ( _Bool , collide_with , Spaceship , Spaceship ) // справиться с космическим кораблем, сталкивающимся с космическим кораблем endmethod // пример использованияint main ( void ) { OBJ a = gnew ( Астероид ); OBJ s = gnew ( Космический корабль ); 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 )); грелэйз ( а ); грелэйз ( s ); }
По состоянию на 2021 год [обновлять]C ++ изначально поддерживает только одиночную диспетчеризацию, хотя в 2007 году Бьярне Страуструп (и его коллеги) предложили добавить несколько методов (множественную диспетчеризацию). [20] Методы обхода этого ограничения аналогичны: используйте либо шаблон посетителя , либо динамическое приведение типов, либо библиотеку:
// Пример использования сравнения типов во время выполнения с помощью dynamic_cast структура Thing { virtual void collideWith ( Thing & other ) = 0 ; }; struct Asteroid : Thing { void collideWith ( Thing & other ) { // dynamic_cast к типу указателя возвращает NULL, если приведение не удалось // (dynamic_cast к ссылочному типу вызовет исключение в случае неудачи) if ( auto asteroid = dynamic_cast < Asteroid *> ( & other )) { // обработка столкновения астероида с астероидом } else if ( auto spaceship = dynamic_cast < Spaceship *> ( & other )) { // обработка столкновения астероида с космическим кораблем } else { // обработка столкновений по умолчанию } } }; struct Spaceship : Thing { void collideWith ( Thing & other ) { if ( auto asteroid = dynamic_cast < Asteroid *> ( & other )) { // обработка столкновения космического корабля с астероидом } else if ( auto spaceship = dynamic_cast < Spaceship *> ( & other )) { // обработка столкновения космического корабля с космическим кораблем } else { // обработка столкновений по умолчанию } } };
или таблица поиска указателей на методы:
#include <cstdint> #include <typeinfo> #include <unordered_map> класс Thing { protected : Thing ( std :: uint32_t cid ) : tid ( cid ) {} const std :: uint32_t tid ; // идентификатор типа 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 ) { 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 ; } статический CollisionHandlerMap collisionCases ; public : void collideWith ( Thing & other ) { auto handler = collisionCases . find ( key ( tid , other . tid )); if ( handler != collisionCases . end ()) { ( this ->* handler -> second )( other ); // вызов указателя на метод } else { // обработка столкновений по умолчанию } } }; class Asteroid : public Thing { void asteroid_collision ( Thing & other ) { /*обработка столкновения астероида с астероидом*/ } void spaceship_collision ( Thing & other ) { /*обработка столкновения астероида с космическим кораблем*/ } public : Asteroid () : Thing ( cid ) {} static void initCases (); static const std :: uint32_t cid ; }; class Spaceship : public Thing { void asteroid_collision ( Thing & other ) { /*обработка столкновения космического корабля с астероидом*/ } void spaceship_collision ( Thing & other ) { /*обработка столкновения космического корабля с космическим кораблем*/ } public : Spaceship () : Thing ( cid ) {} static void initCases (); static const std :: uint32_t cid ; // идентификатор класса }; Thing :: CollisionHandlerMap Thing :: collisionCases ; const std :: uint32_t Астероид :: cid = typeid ( Астероид ) .hash_code (); const std :: uint32_t Космический корабль :: cid = typeid ( Космический корабль ) .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 () { Астероид :: initCases (); Космический корабль :: initCases (); Астероид a1 , a2 ; Космический корабль s1 , s2 ; a1.collideWith ( a2 ) ; a1.collideWith ( s1 ) ; s1.collideWith ( s2 ) ; s1.collideWith ( a1 ) ; }
Библиотека YOMM2 [21] обеспечивает быструю ортогональную реализацию открытых мультиметодов .
Синтаксис объявления открытых методов вдохновлен предложением для собственной реализации C++. Библиотека требует, чтобы пользователь зарегистрировал все классы, используемые в качестве виртуальных аргументов (и их подклассы), но не требует никаких изменений в существующем коде. Методы реализованы как обычные встроенные функции C++; их можно перегружать и передавать указателем. Нет ограничений на количество виртуальных аргументов, и их можно произвольно смешивать с невиртуальными аргументами.
Библиотека использует комбинацию методов (сжатые таблицы диспетчеризации, целочисленную хэш-таблицу без столкновений) для реализации вызовов методов за постоянное время, одновременно снижая использование памяти. Диспетчеризация вызова открытого метода с одним виртуальным аргументом занимает всего на 15–30% больше времени, чем вызов обычной виртуальной функции-члена, когда используется современный оптимизирующий компилятор.
Пример Asteroids можно реализовать следующим образом:
#include <yorel/yomm2/keywords.hpp> #include <память> класс Вещь { public : virtual ~ Вещь () {} }; класс Астероид : публичная вещь { }; класс Космический корабль : public Thing { }; register_classes ( Вещь , Космический корабль , Астероид ); declare_method ( void , collideWith , ( virtual_ < Thing &> , virtual_ < Thing &> )); define_method ( void , collideWith , ( Thing & left , Thing & right )) { // обработка столкновений по умолчанию } define_method ( void , collideWith , ( Asteroid & left , Asteroid & right )) { // обработка столкновения астероида с астероидом } define_method ( void , collideWith , ( Астероид и слева , Космический корабль и справа )) { // обработка столкновения астероида и космического корабля } define_method ( void , collideWith , ( Космический корабль и слева , Астероид и справа )) { // обработка столкновения космического корабля и астероида } define_method ( void , collideWith , ( Spaceship & left , Spaceship & right )) { // обработка столкновения космического корабля с космическим кораблем } int main () { yorel :: yomm2 :: update_methods (); std :: unique_ptr < Thing > a1 ( std :: make_unique < Asteroid > ()), a2 ( std :: make_unique < Asteroid > ()); std :: unique_ptr < Thing > s1 ( std :: make_unique < Spaceship > ()), s2 ( std :: make_unique < Spaceship > ()); // примечание: типы частично стерты collideWith ( * a1 , * a2 ); // Столкновение астероида с астероидом collideWith ( * a1 , * s1 ) ; // Столкновение астероида с космическим кораблем collideWith ( * s1 , * a1 ); // Столкновение космического корабля с астероидом collideWith ( * s1 , * s2 ); // Столкновение космического корабля с космическим кораблем вернуть 0 ; }
Страуструп упоминает в The Design and Evolution of C++ , что ему понравилась концепция мультиметодов, и он рассматривал возможность ее реализации в C++, но утверждает, что не смог найти эффективный пример реализации (сравнимый с виртуальными функциями) и решить некоторые возможные проблемы неоднозначности типов. Затем он заявляет, что хотя эта функция все еще была бы хороша, ее можно приблизительно реализовать с помощью двойной диспетчеризации или таблицы поиска на основе типов, как описано в примере C/C++ выше, поэтому она имеет низкий приоритет для будущих изменений языка. [22]
По состоянию на 2021 год [обновлять], как и многие другие объектно-ориентированные языки программирования, D изначально поддерживает только одиночную диспетчеризацию. Однако в D можно эмулировать открытые мультиметоды как библиотечную функцию. Библиотека openmethods [23] является примером.
// Декларация Matrix plus ( virtual ! Matrix , virtual ! Matrix ); // Переопределение для двух объектов DenseMatrix @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 ; } // Переопределение для двух объектов DiagonalMatrix @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 ) ; }
В языке с единственной диспетчеризацией, таком как Java , множественную диспетчеризацию можно эмулировать с помощью нескольких уровней единичной диспетчеризации:
интерфейс Collideable { void collideWith ( final Collideable other ); /* Эти методы должны были бы иметь разные имена в языке без перегрузки методов. */ void collideWith ( final Asteroid asteroid ); void collideWith ( final Spaceship spaceship ); } class Asteroid implements Collideable { public void collideWith ( final Collideable other ) { // Вызываем collideWith для другого объекта. other . collideWith ( this ); } public void collideWith ( final Asteroid asteroid ) { // Обработка столкновения астероида с астероидом. } public void collideWith ( final Spaceship spaceship ) { // Обработка столкновения астероида и космического корабля. } } class Spaceship implements Collideable { public void collideWith ( final Collideable other ) { // Вызываем collideWith для другого объекта. other . collideWith ( this ); } public void collideWith ( final Asteroid asteroid ) { // Обработка столкновения космического корабля с астероидом. } public void collideWith ( final Spaceship spaceship ) { // Обработка столкновения космического корабля с космическим кораблем. } }
instanceof
Также можно использовать проверки времени выполнения на одном или обоих уровнях.
Множественная диспетчеризация — выбор функции для вызова на основе динамического типа двух или более аргументов — является решением нескольких классических проблем объектно-ориентированного программирования.