Обычно о них не думают как о возможностях объектно-ориентированного программирования, но published методы основаны на RTTI, чтобы можно было выполнять поиск методов, используя строку с именем метода, во время выполнения (run-time). Эта возможность широко используется IDE и VCL при написании обработчиков событий во время разработки.
При создании нового обработчика событий (с помощью двойного щелчка на пустом значении события в инспекторе объектов) или когда вы связываете свойства события с существующим методом (с помощью выпадающего списка или даже просто вводите вручную название метода), IDE гарантирует, что параметры назначаемого метода-обработчика будут соответствовать параметрам типа события. Аналогичным образом, когда вы назначаете свойству события обработчик в коде, то компилятор выполняет проверку во время компиляции, что у свойства и обработчика соответствуют параметры и соглашение о вызовах.
В run-time подобных проверок нет. Все присвоенные в design-time события хранятся в файле .DFM просто указанием строки с именем метода. Когда .DFM загружается в память в run-time, нужный метод ищется функцией
TObject.MethodAddress
– см. TReader.FindMethod
в Classes.pas
для получения подробностей.Функция
TObject.MethodAddress
делает свою магию путём сканирования волшебных таблиц компилятора, известных как таблицы методов (Method Table) или Published Method Table - как предпочитаю называть их я, уменьшая возможную путаницу с таблицами динамических и виртуальных методов.Включение RTTI
По умолчанию расширенная информация о типе во время выполнения (RTTI) для класса отключена. В отличие от .NET, где мета-данные создаются для всех членов, в Delphi RTTI создаётся только на published члены, когда класс компилируется с директивой компилятора{$M+}
или когда он является дочерним к классу, который скомпилировали в режиме {$М+}
(например, TPersistent
, TComponent
и т.д.). Для {$М+}
есть альтернатива с длинным именем - {$TypeInfo ON}
. Для целей нашего обсуждения, я буду называть такие классы классами MPlus, а все остальные классы - классами MMinus.В дополнение к явным published членам, все члены класса MPlus в самой верхней части его объявления класса, которая не имеет явного спецификатора видимости, также считаются published. Для MMinus классов, эти члены являются просто public. Именно поэтому все компоненты и обработчики событий в верхней части форм являются published (
TForm
является MPlus классом).Компилятор позволяет публиковать (publish) поля типа объектной и интерфейсной ссылки, свойства большинства типов и методы. В этой статье мы сделаем основной упор на published методы. Давайте напишем небольшую тестовую программу для работы с published членами и MPlus и MMinus классов.
program TestMPlus; {$APPTYPE CONSOLE} uses Classes, SysUtils, TypInfo; type {$M-} TMMinus = class DefField: TObject; property DefProp: TObject read DefField write DefField; procedure DefMethod; published PubField: TObject; property PubProp: TObject read PubField write PubField; procedure PubMethod; end; {$M+} TMPlus = class DefField: TObject; property DefProp: TObject read DefField write DefField; procedure DefMethod; published PubField: TObject; property PubProp: TObject read PubField write PubField; procedure PubMethod; end; procedure TMMinus.DefMethod; begin end; procedure TMMinus.PubMethod; begin end; procedure TMPlus.DefMethod; begin end; procedure TMPlus.PubMethod; begin end; procedure DumpMClass(AClass: TClass); begin Writeln(Format('Testing %s:', [AClass.Classname])); Writeln(Format('DefField=%p', [AClass.Create.FieldAddress('DefField')])); Writeln(Format('DefProp=%p', [TypInfo.GetPropInfo(AClass, 'DefProp')])); Writeln(Format('DefMethod=%p', [AClass.MethodAddress('DefMethod')])); Writeln(Format('PubField=%p', [AClass.Create.FieldAddress('PubField')])); Writeln(Format('PubProp=%p', [TypInfo.GetPropInfo(AClass, 'PubProp')])); Writeln(Format('PubMethod=%p', [AClass.MethodAddress('PubMethod')])); Writeln; end; begin DumpMClass(TMMinus); DumpMClass(TMPlus); ReadLn; end.
Причуда компилятора
Целью этой тестовой программы является проверка, что RTTI создаётся для умалчиваемой и published видимости для MPlus классов и что RTTI не создаётся для MMinus классов. У нас есть два классаTMMinus
и TMPlus
, которые имеют одинаковый набор членов, но собираются в разных режимах $M
. Можно было бы ожидать, что TMMinus
вообще не будет иметь RTTI для своих членов, а TMPlus
будет иметь RTTI для всех его членов.Подпрограмма
DumpMClass
выводит raw-указатели по RTTI для полей, свойств и методов каждого класса. Когда мы запускаем эту программу, то мы получим этот удивительный результат:
Testing TMMinus: DefField=00000000 DefProp=00000000 DefMethod=00000000 PubField=008C0A78 PubProp=00000000 PubMethod=00412898 Testing TMPlus: DefField=008C0AA0 DefProp=00412852 DefMethod=0041289C PubField=008C0AF4 PubProp=00412874 PubMethod=004128A0Как и ожидалось - класс
TMPlus
имеет RTTI информацию для всех шести членов, доказывая, что $M+
включает RTTI и что областью видимости по умолчанию для класса MPlus становится published. Странной же вещью в этом выводе является то, что класс TMMinus
, хотя и объявлен с выключенным TYPEINFO, но у него всё ещё есть RTTI информация для двух его членов - поля и метода, явно записанными в published. Эта реальность противоречит документации, которая говорит:
Класс не может иметь published члены, если только он не скомпилирован в режимеЭто, вероятно, баг компилятора. Заметьте, что published свойство не получает RTTI. Документация звучит так, словно компилятор сгенерирует ошибку компиляции (или хотя бы предупреждение), если вы вставите published секцию в MMinus класс. Но это не так.{$M+}
или происходит от класса, скомпилированного в{$M+}
. Большинство классов с published членами происходят отTPersistent
, который скомпилирован в режиме{$M+}
, так что необходимость использования директивы$M
встречается достаточно редко.
Прим.пер.: не уверен, в какой версии Delphi/справки это проверялось, но в современных версиях ситуация такова, что RTTI генерируется для всех членов секции published и для MPlus и для MMinus классов:
A published member has the same visibility as a public member, but the compiler generates runtime type information for published members.Т.е. "Члены published имеют ту же видимость, что и члены public, но компилятор дополнительно генерирует для них RTTI". Более того, у компилятора на этот случай даже есть предупреждение: "Published caused RTTI ($M+) to be added to type '%s'". Иными словами: да, баг здесь есть, но только в том, что
PubProp
не имеет RTTI.Умалчиваемая видимость членов класса документирована так:
Члены в начале объявления класса, которые не имеют явно указанную видимость по умолчанию являются published - при условии, что класс компилируется в состоянииЭто полностью соответствует тому, что мы видели в нашем небольшом эксперименте. К счастью, члены класса MMinus без спецификатора видимости не генерируют неожиданной RTTI.{$M+}
или является производным от класса, скомпилированного в{$M+}
, в противном случае такие члены имеют видимость public.
Полиморфное использование published методов
Хотя на это можно смотреть как на хак, но вы можете использовать published методы для реализации очень простого и очень гибкого полиморфного механизма диспетчеризации с поздним связыванием. Он гибкий потому, что вызывающий и вызываемый не обязаны что-либо знать друг о друге или иметь общий интерфейс. Вызывающему нужно знать имя, параметры и соглашение вызова метода, который он хочет вызвать, а вызываемый должен реализовать этот метод как published метод с нужным именем, параметрами и соглашением вызова.Чтобы заместить (override) существующий published метод, класс-наследник просто определит новый published метод с тем же именем. При этом метод не обязан быть ни динамическим, ни виртуальным. Поскольку поиск метода всегда начинается с текущего класса, то это будет работать очень похоже на полиморфное поведение динамических методов.
Давайте посмотрим на простой пример:
program TestPolyPub; {$APPTYPE CONSOLE} uses Classes, SysUtils, TypInfo, Contnrs; type {$M+} TParent = class published procedure Polymorphic(const S: string); end; TChild = class(TParent) published procedure Polymorphic(const S: string); end; TOther = class published procedure Polymorphic(const S: string); end; procedure TParent.Polymorphic(const S: string); begin Writeln('TParent.Polymorphic: ', S); end; procedure TChild.Polymorphic(const S: string); begin Writeln('TChild.Polymorphic: ', S); end; procedure TOther.Polymorphic(const S: string); begin Writeln('TOther.Polymorphic: ', S); end; function BuildList: TObjectList; begin Result := TObjectList.Create; Result.Add(TParent.Create); Result.Add(TChild.Create); Result.Add(TOther.Create); end; type TPolymorphic = procedure (Self: TObject; const S: string); procedure CallList(List: TObjectList); var i: integer; Instance: TObject; Polymorphic: TPolymorphic; begin for i := 0 to List.Count-1 do begin Instance := List[i]; // Синтаксис присвоили-и-вызвали Polymorphic := Instance.MethodAddress('Polymorphic'); if Assigned(Polymorphic) then begin Polymorphic(Instance, IntToStr(i)); // Альтернативный синтаксис всё-в-одном: TPolymorphic(Instance.MethodAddress('Polymorphic'))(Instance, IntToStr(i)); end; end; end; begin CallList(BuildList); ReadLn; end.Здесь мы сначала определим три класса - каждый с published методом, названным
Polymorphic
, который принимает один строковый параметр (в дополнение к неявному параметру Self
) и использует соглашение по умолчанию register
. Два класса наследуют друг друга, и класс TChild
на практике замещает метод Polymorphic
, который он унаследовал от TParent
. Класс TOther
никак не связан с двумя другими классами (ну, хотя все они наследуются от TObject
), но его метод Polymorphic
ровно так же может быть вызван "виртуально".Затем мы строим гетерогенный список объектов, в котором содержится каждый из трёх классов. Этот список передается процедуре
CallList
, которая находит и вызывает published метод Polymorphic
каждого экземпляра в списке. Язык Delphi не имеет встроенного синтаксиса для вызова published метода через строку имени, поэтому мы должны вручную присвоить результат поиска метода по имени от Instance.MethodAddress
в процедурную переменную, а затем вызвать метод через переменную. Кроме того, мы можем объединить эти операции в один оператор, который делает приведение типа результата MethodAddress
в правильный процедурный тип вызываемого метода и вызывает сам метод. Оба синтаксиса показаны выше.Интересная особенность вызова published методов: вы можете проверить в run-time, имеет ли заданный экземпляр класса конкретный метод или нет. Таким образом, вы можете использовать published методы для реализации дополнительного поведения или обратных вызовов. Например, обобщённая потоковая система может дополнительно вызывать published методы
BeginStreaming
и EndStreaming
до и после сериализации экземпляра класса. Только те классы, которым необходимо проводить специальные действия, будут реализовывать (и объявлять) эти метода. Published методы могут даже использоваться как аналог атрибутов класса "для бедняков" (прим.пер.: т.е. реализация аналога атрибутов классов в тех версиях Delphi, где они не поддерживаются).Основной недостаток этого метода заключается в том, что нет никаких проверок сигнатур методов ни во время проектирования, ни во время выполнения. Если вы вызываете метод с отличным соглашением вызова, либо несовпадающими типами или количеством параметров, во время выполнения могут происходить "интересные" вещи.
Два класса наследуют друг друга, и класс TChild на практике замещает метод Polymorphic, который он унаследовал от TParent
ОтветитьУдалитьЧто-то я не вижу чтобы TChild наследовался бы от TParent. Все три класса (включая TOther) наследуются от класса по-умолчанию (т.е. от TObject)... :)
Спасибо, исправил!
ОтветитьУдалитьСпасибо, очень помогло. Кто бы мог подумать, что MethodAddress находит только прописанные в published методы. Именно при работе в run-time и билде в D2010 директива {M+}/{M-} на поиск не влияет.
ОтветитьУдалить