вторник, 19 июля 2011 г.

Published методы

Это перевод Published methods. Автор: Hallvard Vassbotn.

Обычно о них не думают как о возможностях объектно-ориентированного программирования, но 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 члены, если только он не скомпилирован в режиме {$M+} или происходит от класса, скомпилированного в {$M+}. Большинство классов с published членами происходят от TPersistent, который скомпилирован в режиме {$M+}, так что необходимость использования директивы $M встречается достаточно редко.
Это, вероятно, баг компилятора. Заметьте, что published свойство не получает RTTI. Документация звучит так, словно компилятор сгенерирует ошибку компиляции (или хотя бы предупреждение), если вы вставите published секцию в MMinus класс. Но это не так.

Прим.пер.: не уверен, в какой версии 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 - при условии, что класс компилируется в состоянии {$M+} или является производным от класса, скомпилированного в {$M+}, в противном случае такие члены имеют видимость public.
Это полностью соответствует тому, что мы видели в нашем небольшом эксперименте. К счастью, члены класса MMinus без спецификатора видимости не генерируют неожиданной RTTI.

Полиморфное использование 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, где они не поддерживаются).

Основной недостаток этого метода заключается в том, что нет никаких проверок сигнатур методов ни во время проектирования, ни во время выполнения. Если вы вызываете метод с отличным соглашением вызова, либо несовпадающими типами или количеством параметров, во время выполнения могут происходить "интересные" вещи.

3 комментария:

  1. Два класса наследуют друг друга, и класс TChild на практике замещает метод Polymorphic, который он унаследовал от TParent
    Что-то я не вижу чтобы TChild наследовался бы от TParent. Все три класса (включая TOther) наследуются от класса по-умолчанию (т.е. от TObject)... :)

    ОтветитьУдалить
  2. Спасибо, очень помогло. Кто бы мог подумать, что MethodAddress находит только прописанные в published методы. Именно при работе в run-time и билде в D2010 директива {M+}/{M-} на поиск не влияет.

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.

Примечание. Отправлять комментарии могут только участники этого блога.