У мові Object Pascal, що використовується середовищем розробки Delphi, існує механізм віртуальних конструкторів. Шанувальникам C ++ це здається жахливою єрессю, але віртуальні конструктори дуже зручні для створення екземплярів класів, які ще не визначені на етапі компіляції створює коду. Така технологія дозволяє розробляти компонентний код без необхідності реалізації фабрик класів.
Зворотним боком цієї гнучкості є можливість випадково створити екземпляр абстрактного класу, що згодом майже неминуче призведе до виклику одного з абстрактних методів. Якщо в C ++ виклик чисто віртуальної функції вимагає неабиякої спритності, і практично неможливо зробити його ненавмисно, то в Delphi це досягається одним необережним рухом. На жаль, вбудовані в Delphi механізми виявлення та обробки абстрактних методів надають лише мінімум інформації про джерело помилки.
Віртуальні конструктори
Всі приклади в цій статті, якщо не вказано інше, компілювалися і тестувалися на Borland Delphi 5.
У поєднанні з посиланням на клас компонента:
це дозволяє створювати будь-які компоненти, маючи посилання на їхній клас, навіть якщо він розроблений після компіляції наступного коду:
Така можливість є ключовою для роботи механізму читання форм з DFM-файлів і ресурсів. Крім того, вона може бути корисною і для призначеного для користувача коду, який не пов'язаний безпосередньо з VCL. Найбільш популярні області застосування подібної функціональності - це сериализация об'єктів і реєстрація plug-in. Крім цього, на основі цього механізму і RTTI в Delphi 6 реалізовані веб-сервіси.
абстрактні методи
Розглянемо тепер наступний код:
На перший погляд, все в порядку. Є абстрактний клас, який декларує якусь функціональність, і є його нащадок, який реалізує цю функціональність. Ми припускаємо використовувати це приблизно таким чином:
(Реальний код, звичайно, буде трохи складніше. Швидше за все, об'єкти будуть створюватися в одному місці, а використовуватися в іншому, але суть справи це не змінює.)
У чому ж проблема? А в тому, що недбайливий прикладний програміст запросто може передати в нашу процедуру як параметр посилання на абстрактний клас:
Такий код цілком вдало скомпілюється. Що ж станеться під час роботи програми? Найпростіший експеримент покаже, що результатом буде видача виключення EAbstractError в момент виклику методу DoSomeJob.
Здавалося б, все в порядку: порушник спійманий, справедливість відновлена. Але ж ні. EAbstractError - на рідкість неінформативне клас. Він не надає ніякої інформації про контекст і причини помилки. Якщо ви - розробник, і додаток видало відповідне повідомлення, то у вас є шанс витратити деякий час на спілкування з отладчиком і послідовне виконання, щоб відловити клас-порушник. Але якщо ви скомпілювали свою бібліотеку без налагоджувальної інформації і вихідних текстів, то прикладний програміст зможе тільки гадати, що ж він зробив не так.
Є, звичайно, дуже простий спосіб «обійти» проблему - ніколи не оголошувати абстрактних методів. VCL використовує «порожні» визначення методів часто-густо. Однак це не шлях для справжніх програмістів. Хоча б з тієї причини, що «порожня» реалізація процедури ще має якийсь сенс, але будь-яка функція повинна повертати якесь значення.
Більш природним способом є заборона на створення екземплярів абстрактних класів, як це зроблено, наприклад, в C ++. На жаль, компілятор Delphi обмежиться попередженням: "constructing instance of class ... containing abstract methods". Висновок цього попередження можна придушити відповідними опціями компілятора.
Як правило, акуратні програмісти уважно стежать за попередженнями, що видаються компілятором. Але в ситуації, яка описана вище, «грім не вдарить» і причин хреститися у програміста не буде.
тестове додаток
Проілюструємо техніку використання особливостей об'єктної моделі Object Pascal на прикладі нескладного додатку.
Наша програма буде гранично простий. Вона дозволить користувачеві ввести два цілих числа і зробити з ними набір простих арифметичних операцій. У реалізацію програми буде входити тільки додавання і множення, але ми подбаємо про те, щоб програмісти могли допомагати користувачеві програми йти в ногу з часом, розробляючи доповнення до програми.
Для перевірки концепції ми створимо пакет розширення, в якому реалізуємо два класи складних цілочисельних операторів: TPowerOp - оператор піднесення до степеня і TCnkOp - оператор кількості поєднань.
У класі TCnkOp ми «забудемо» перекрити один з абстрактних методів, оголошених в базовому класі. Ми переконаємося, що стандартна обробка таких помилок не дає ніякої інформації про причини виникнення помилки, і побудуємо свою обробку так, щоб можна було відразу визначити, в якому класі і який метод був залишений абстрактним.
Отримати додаткову інформацію
Щоб дізнатися більше про те, що привело до абстрактного викликом, необхідно розібратися з тим, як Delphi реалізує обробку абстрактних методів.
Стандартний оброблювач
Якщо протрассировать виклик абстрактного методу TAbstractObject.DoSomeJob, то з'ясується цікава подробиця: управління передається в системну процедуру _AbstractError:
вдосконалений обробник
З попереднього розділу можна зробити два висновки:
- Існує документований спосіб зареєструвати свій обробник абстрактних викликів.
- Незважаючи на те, що середовище не передає в цей обробник ніяких параметрів, функції, які викликають наш обробник, не впливають на контекст виклику.
Зверніть увагу на код процедури TAbstractHandler.HandleAbstract - він генерує виняток з ім'ям класу в якості тексту повідомлення. На перший погляд здається, що він завжди буде повертати рядок "TAbstractHandler", але це не так. Справа в тому, що ми викликали метод TAbstractHandler.HandleAbstract на об'єкті зовсім іншого класу! Фактично виконується код дуже схожий на ось такий:
В такому прикладі текст виключення буде містити "TAbstractObject". Зазвичай подібні виклики призводять до помилок, але при дотриманні деяких правил вони цілком безпечні. «Песимістична» версія цих правил така: викликати «чужий» метод можна тільки в тому випадку, якщо він користується тільки полями і методами загального предка «свого» і «чужого» класу. На практиці свободи більше, але для нашого випадку її вже цілком достатньо. Метод HandleAbstract користується тільки методом ClassName, доступним в TObject, який гарантовано є предком всіх класів Delphi.
Ця методика не працює при виклику абстрактного методу класу. У методах класу self вказує на клас, а не на об'єкт, і використовувана підміна некоректна. На жаль, надійного способу боротьби з цим я не бачу - досить-таки складно відрізнити покажчик на VMT від покажчика на покажчик на VMT.
раннє попередження
Щоб запобігти створенню примірників абстрактних класів, треба, перш за все, відповісти на питання: «чи є даний клас абстрактним?». Відповідь на це питання проста: «клас є абстрактним, якщо він містить абстрактні методи». Сама Delphi не містить вбудованих засобів для перевірки методів на абстрактність, тому такі кошти доведеться винайти самостійно.
Щоб дізнатися, абстрактний чи метод класу, доведеться трохи покопатися в темних глибинах модуля System за допомогою покрокової налагодження. Як ми вже знаємо з попереднього розділу, спроба викликати абстрактний метод призводить нас в процедуру _AbstractError. Тепер нам необхідно простежити шлях, що веде в цю процедуру.
Дослідження структури таблиці віртуальних методів (VMT), створюваної компілятором, і RTTI взагалі, є найцікавішим процесом, який може доставити допитливому розробнику масу задоволення. Для тих же, хто не хоче втрачати час на препарування системного коду Delphi, я привожу необхідну інформацію в готовому до вживання вигляді.
Структура класів Delphi
- Порівнювати посилання на клас для перевірки типу об'єкта.
- Викликати методи класу.
- Викликати конструктори.
На жаль, цієї функціональності недостатньо для пошуку абстрактних методів. Для такого пошуку нам доведеться заглянути «під капот» класу, а саме - подивитися, як працює метод TObject.ClassType. Реалізація, звичайно, може змінюватися від версії до версії. У Delphi 5 код гранично лаконічний:
Як цікаво! Частина з них менше нуля. Судячи з імен констант, аж до vmtAfterConstruction (зміщення -28) розташовані покажчики на різні цікаві дані. Потім йдуть покажчики на віртуальні методи, декларовані в самому TObject: AfterConstruction, BeforeDestruction, Dispatch, DefaultHandler, NewInstance, FreeInstance, Destroy. Потім йдуть методи з невід'ємними зсувами. Таким чином, покажчик, розташований на початку об'єкта, посилається кудись «в середину» VMT. І ця середина - рівно те місце, з якого будуть розташовуватися віртуальні методи, оголошені в класах-нащадках. З назв констант vmtQueryInterface, vmtAddRef і vmtRelease ясно, навіщо так зроблено - інакше в нащадках TObject було б неможливо реалізувати інтерфейс IUnknown.
Отже, 4 байта, отриманих при виклику TObject.ClassType, вказують на початок таблиці віртуальних методів, декларованих в нащадках TObject. Цей висновок можна вважати «безпечним» до тих пір, поки Delphi підтримує сумісність з COM.
абстрактні методи
Цей код вимагає деяких пояснень.
Отримавши покажчик на метод класу в змінної TAP, ми виділяємо з нього покажчик на код за допомогою документованого приведення до типу SysUtils.TMethod.
Однак ці експерименти ми проводили над класом, який скомпільовано, як частина нашого застосування. У нашому ж прикладі частина класів розташована в окремому пакеті, який компілюється в окремий файл-бібліотеку. Чи буде відбуватися виклик тій же _AbstractProc з таких класів? І якщо буде, то як?
Для отримання відповіді на ці питання необхідно знати про те, як Delphi реалізує динамічно підключаються пакети компонентів. Детальний розгляд цієї теми виходить за межі даної статті. Тому я відразу надам тут результат, пропустивши опис своїх досліджень .bpl-файлів.
Так, Delphi строго стежить за тим, щоб в додаток можна було завантажити дві версії одного і того ж модуля в різних пакетах. Тобто ми можемо бути впевнені, що будь-який абстрактний виклик призведе нас в єдину _AbstractProc. Для цього він користується механізмом таблиць імпорту, наданим форматом PE-файлів Windows. На практиці це означає, що відповідна позиція в VMT буде вказувати на фрагмент коду (thunk) наступного вигляду: