Неймовірні пригоди у коді

Вчора я отримав цікаве запитання:

Якщо у тебе є перевантажений operator ==, то кожен виклик цього оператора піддається «ранньому зв'язуванню»під час компіляціївідповідно доформальнихтипівоперандів. Але метод Equals() об'єкта викликається як віртуальний; даний метод визначаєтьсяпід час виконаннявідповідно дофактичноготипу одержувача. Ця різниця здається мені кривавою. Який принцип дизайну тут доречний?

Коротка відповідь – у тому, що дизайнери мов та дизайнери бібліотек по-різному дивляться на завдання.

Перевантаження оператора == – це мовна угода; метод Equals() – це угода бібліотеки. Вони відрізняються тому, що їхні дизайнери по-різному мислили над завданням, виходили з різних вимог і мали різні інструменти для вирішення завдання.

Довга відповідь - у тому, що тут все криво і ніщо не працює так, як мало б в ідеалі.

Перед тим, як я продовжу, мені потрібно коротко визначити, що я маю на увазі під різними способами диспетчеризації методів:

Прине віртуальної диспетчеризації, дозвіл навантажень «статично» визначає, який саме метод буде викликатися, ґрунтуючись на формальних типах аргументів та формальному типі одержувача.

Віртуальна диспетчеризаціястатично визначає, який із «слотів» буде викликатися, ґрунтуючись на формальних типах аргументів і формальному типі одержувача, але сам метод для виклику визначається під час виконання залежно від вмісту слота. А вміст слота залежить від фактичного типу одержувача.

Це те, що ми називатимемо «одинарна віртуальна диспетчеризація». Тобто, ми розглядаємо фактичний тип тільки уодержувача . Фактичні типиаргументівзначення не мають. Можна уявити собі мову, в якій вони були б важливими, придумавши нову «множинно-віртуальну» диспетчеризацію:

class B publicmultivirtualvoid M(object x) < > >

class D : B < publicmultioverridevoid M(object x)< > publicmultioverridevoid M(string x) < > >

object argument = "hello"; B receiver = new D(); receiver.M(argument);

У мові з одиночно-віртуальною диспетчеризацією це призведе до викликупершого методуD, тому що тільки фактичний тип одержувача важливий при диспетчеризації виклику. У множинно-віртуальній мові ми могли б зробити тут те, що називається «подвійна віртуальна диспетчеризація», спрямовуючи виклик удругий методD, використовуючи фактичні типи як одержувача ,так іаргументу.

Звичайно, вам не потрібно це уявляти, оскільки можна переконатися самим. Є мови, які підтримують «мультиметоди», тобто методи, які виконують одинарну, подвійну, потрійну тощо, віртуальну диспетчеризацію. Першим на думку спадає Dylan, хоча є багато інших.

Тим не менш, у CLR «вшитий» механізм для ефективної одинарної диспетчеризації, але не вшито механізмів для ефективної множинної диспетчеризації. (Якщо ви дійсно хочете множинну диспетчеризацію, то, в принципі, в C # 4 можете використовувати «dynamic», і ми зробимовесьзвичайний аналіз часу компіляції під час виконання. Це не дуже ефективно в порівнянні з одним розіменуванням покажчика для одиночної диспетчеризації, але робить те, що потрібно, і з прийнятною у багатьох випадках продуктивністю).

Який стосунок це має до порівнянь?

Щобвідповісти на задане питання, спочатку потрібно зрозуміти «чому Equals диспетчеризується одинарно-віртуально?» Гарне питання!В ідеальному світі тут була б подвійна віртуальна диспетчеризація!Коли ви порівнюєте два об'єкти на предмет еквівалентності за допомогою методу Equals,звичайно ж тип аргументу до останнього біта важливий не менше, ніж у одержувача! Ми всі вважаємо еквівалентністьсиметричноюоперацією, алеEqualsглибоко асиметричний. Ліва частина Equals – одержувач – обробляється особливим чином, а права частина – аргумент – ні.

Ось чому більшість нетривіальних реалізацій Equals зводяться до саморобної логіки подвійної диспетчеризації. Найкраща реалізація методу Equals доступна для лівої частини вибирається за допомогою одинарної диспетчеризації, а потім ця реалізація відповідає за з'ясування фактичного типу аргументу і виконання того, що потрібно, якщо їй не все одно. Це спрощена версія більш загального патерну; подвійна диспетчеризація часто реалізується поверх одинарної шляхом реалізаціїпатерну «відвідувач». (Пошукайте в мережі за цими словами, якщо вам цікаві способи зробити подвійну диспетчеризацію у мовах із підтримкою одинарної).

Жоден із варіантів не є безумовним переможцем – дизайн, зрештою, це мистецтво приймати гарні компромісні рішення. З погляду дизайнера бібліотек, (2) – найкращий варіант. Без величезних архітектурних змін у CLR, але з максимумом гнучкості для розробника ціною деякої додаткової роботи для них. І ця робота «не їдеш – не плати» - розробники, яким все одно можуть просто взяти реалізацію за замовчуванням і ніколи про це не думати.

Загалом це не так погано. Лексично, виклик x.Equals(y) виглядає як виклик віртуального методу, тому мишвидше за все чекатимемо від нього одинарно-віртуальної диспетчеризації, як і від будь-якого іншого віртуального виклику.

Але використання оператора == - це окремий город. Користувачі розумно вважають, що оператор = симетричний. Ця річ не виглядає як виклик методу.

Для дизайнера мови, варіант 3 значно привабливіший, ніж будь-який інший. Тому його ми й обрали.

Аналіз оператора рівності C# симетричний; ми шукаємо перевантажені оператори в ієрархіях типів обох операндів, кидаємо їх усі в один кошик, і намагаємося вибрати «найкращого» кандидата. Якщо ми не можемо знайти єдиного найкращого, то видаємо помилку.

У C# 4.0, якщо операнди == позначені «dynamic», ми виконаємо весь звичайний аналіз часу компіляціїпід час виконання, грунтуючись нафактичнихтипах, щов результаті> дає вам щось типу двічі-віртуальної диспетчеризації, ціною виклику компілятора під час виконання. Ми кешуємо результат аналізу, тому при другому проході за місцем виклику ефективність буде прийнятною.