Ссылки на классы Delphi, также известные как метаклассы, когда их использовать

Я прочитал официальную документацию и понимаю, что такое ссылки на классы, но не могу понять, когда и почему они являются лучшим решением по сравнению с альтернативами.

В документации приведен пример TCollection, который может быть создан с любым потомком TCollectionItem. Обоснованием использования ссылки на класс является то, что она позволяет вам вызывать методы класса, тип которых неизвестен во время компиляции (я предполагаю, что это время компиляции TCollection). Я просто не понимаю, чем использование TCollectionItemClass в качестве аргумента превосходит использование TCollectionItem. TCollection по-прежнему сможет содержать любого потомка TCollectionItem и по-прежнему иметь возможность вызывать любой метод, объявленный в TCollectionItem. Не так ли?

Сравните это с общей коллекцией. TObjectList, похоже, предлагает те же функции, что и TCollection, с дополнительным преимуществом безопасности типов. Вы освобождаетесь от необходимости наследовать от TCollectionItem для хранения вашего типа объекта, и вы можете сделать коллекцию настолько специфичной для типа, насколько хотите. И если вам нужно получить доступ к членам элемента из коллекции, вы можете использовать общие ограничения. Помимо того факта, что ссылки на классы доступны программистам до Delphi 2009, есть ли какие-либо другие веские причины использовать их вместо общих контейнеров?

Другой пример, приведенный в документации, - это передача ссылки на класс функции, которая действует как фабрика объектов. В данном случае фабрика для создания объектов типа TControl. Это не совсем очевидно, но я предполагаю, что фабрика TControl вызывает конструктор переданного ей типа-потомка, а не конструктор TControl. Если это так, то я начинаю видеть хоть какую-то причину использования ссылок на классы.

Итак, я думаю, что я действительно пытаюсь понять, когда и где ссылки на классы наиболее подходят и что они покупают разработчику?


person Kenneth Cochran    schedule 30.07.2010    source источник
comment
Если вы заменяете TCollection на TObjectList, значит, вы в любом случае не использовали TCollection для того, для чего он был разработан. Он предназначен для случаев, когда вам нужно отредактировать список вещей во время разработки в инспекторе объектов.   -  person Rob Kennedy    schedule 30.07.2010
comment
@Rob Я использовал TCollection в качестве примера, потому что это используется в официальной документации Delphi. Цель моего вопроса - лучше понять ссылки на классы.   -  person Kenneth Cochran    schedule 31.07.2010


Ответы (5)


МетаКлассы и процедуры классов

MetaClasses - это все о процедурах класса. Начиная с базового class:

type
  TAlgorithm = class
  public
    class procedure DoSomething;virtual;
  end;

Поскольку DoSomething является class procedure, мы можем вызывать его без экземпляра TAlgorithm (он ведет себя как любая другая глобальная процедура). Мы можем сделать это:

TAlgorithm.DoSomething; // this is perfectly valid and doesn't require an instance of TAlgorithm

Как только мы получим эту настройку, мы можем создать несколько альтернативных алгоритмов, все из которых будут разделять некоторые части базового алгоритма. Нравится:

type
  TAlgorithm = class
  protected
    class procedure DoSomethingThatAllDescendentsNeedToDo;
  public
    class procedure DoSomething;virtual;
  end;

  TAlgorithmA = class(TAlgorithm)
  public
    class procedure DoSomething;override;
  end;

  TAlgorithmB = class(TAlgorithm)
  public
    class procedure DoSomething;override;
  end;

Теперь у нас есть один базовый класс и два дочерних класса. Следующий код вполне допустим, потому что мы объявили методы как методы класса:

TAlgorithm.DoSomething;
TAlgorithmA.DoSomething;
TAlgorithmB.DoSomething;

Давайте представим тип class of:

type
  TAlgorithmClass = class of TAlgorithm;

procedure Test;
var X:TAlgorithmClass; // This holds a reference to the CLASS, not a instance of the CLASS!
begin
  X := TAlgorithm; // Valid because TAlgorithmClass is supposed to be a "class of TAlgorithm"
  X := TAlgorithmA; // Also valid because TAlgorithmA is an TAlgorithm!
  X := TAlgorithmB;
end;

TAlgorithmClass - это тип данных, который можно использовать, как и любой другой тип данных, он может храниться в переменной и передаваться в качестве параметра функции. Другими словами, мы можем сделать это:

procedure CrunchSomeData(Algo:TAlgorithmClass);
begin
  Algo.DoSomething;
end;

CrunchSomeData(TAlgorithmA);

В этом примере процедура CrunchSomeData может использовать любой вариант алгоритма, если он является потомком TAlgorithm.

Вот пример того, как это поведение может быть использовано в реальном приложении. Представьте себе приложение для расчета заработной платы, в котором некоторые числа должны быть рассчитаны в соответствии с алгоритмом, определенным законом. Вполне возможно, что этот алгоритм со временем изменится, потому что Закон несколько раз менялся. Нашему приложению необходимо рассчитывать заработную плату как за текущий год (с помощью современного калькулятора), так и за другие годы, используя старые версии алгоритма. Вот как это может выглядеть:

// Algorithm definition
TTaxDeductionCalculator = class
public
  class function ComputeTaxDeduction(Something, SomeOtherThing, ThisOtherThing):Currency;virtual;
end;

// Algorithm "factory"
function GetTaxDeductionCalculator(Year:Integer):TTaxDeductionCalculator;
begin
  case Year of
    2001: Result := TTaxDeductionCalculator_2001;
    2006: Result := TTaxDeductionCalculator_2006;
    else Result := TTaxDeductionCalculator_2010;
  end;
end;

// And we'd use it from some other complex algorithm
procedure Compute;
begin
  Taxes := (NetSalary - GetTaxDeductionCalculator(Year).ComputeTaxDeduction(...)) * 0.16;
end;

Виртуальные конструкторы

Конструктор Delphi работает так же, как функция класса; Если у нас есть метакласс, и метакласс знает о виртуальном конструкторе, мы можем создавать экземпляры типов-потомков. Это используется редактором IDE TCollection для создания новых элементов, когда вы нажимаете кнопку «Добавить». Все, что требуется TCollection для этой работы, - это MetaClass для TCollectionItem.

person Cosmin Prund    schedule 30.07.2010
comment
Мне любопытно, почему кто-то проголосовал за этот ответ. Космин, очевидно, вложил в это много мыслей и усилий. Если, конечно, это явно не неверно или не вводит в заблуждение, а это не так, голосование "против" было необоснованным. - person Kenneth Cochran; 31.07.2010
comment
Он, он! Мой первый голос против! Ага ... Я действительно приложил усилия, пытаясь объяснить всю концепцию метакласса до более глубокой концепции, превратив виртуальные конструкторы в частный случай чего-то большего. Я также намеренно привел пример, которого НЕТ в документации (в конце концов, все знают, что в документации). Но послушайте, вот как работает SO. Иногда меня поражает, за что проголосуют, особенно когда за ответы, отмеченные автором как неработающие, проголосуют. Так почему бы не проголосовать против хорошего ответа? - person Cosmin Prund; 31.07.2010
comment
Обычным виртуальным методам для работы требуется экземпляр, виртуальные методы класса работают без экземпляра. Но я признаю, что это очень редко. - person Cosmin Prund; 01.08.2010
comment
Собственно, в Delphi это очень распространено. Лучшим примером является TComponent, чей виртуальный конструктор вызывается каждый раз, когда компонент загружается из потока (например, когда создается / загружается форма и все ее компоненты загружаются из потока). - person davidmw; 09.02.2012

Да, коллекция по-прежнему сможет содержать любого потомка TCollectionItem и вызывать на нем методы. НО, он не сможет создать новый экземпляр любого потомка TCollectionItem. Вызов TCollectionItem.Create создает экземпляр TCollectionItem, тогда как

private
  FItemClass: TCollectionItemClass;
...

function AddItem: TCollectionItem;
begin
  Result := FItemClass.Create;
end;

создаст экземпляр любого класса потомка TCollectionItem, хранящегося в FItemClass.

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

Например, наблюдаемый потомок TObjectList (или общий контейнер) может иметь что-то вроде:

function AddItem(aClass: TItemClass): TItem;
begin
  Result := Add(aClass.Create);
  FObservers.Notify(Result, cnNew);
  ...
end;

Я думаю, что вкратце преимущество / преимущество метаклассов - это любой метод / класс, имеющий только знание

type
  TMyThing = class(TObject)
  end;
  TMyThingClass = class of TMyThing;

может создавать экземпляры любого потомка TMyThing, где бы они ни были объявлены.

person Marjan Venema    schedule 30.07.2010
comment
Это лучше, чем мое объяснение. - person Warren P; 30.07.2010

Обобщения очень полезны, и я согласен, что TObjectList<T> (обычно) более полезен, чем TCollection. Но ссылки на классы более полезны для разных сценариев. Они действительно являются частью другой парадигмы. Например, ссылки на классы могут быть полезны, когда у вас есть виртуальный метод, который необходимо переопределить. Переопределения виртуальных методов должны иметь ту же сигнатуру, что и исходные, поэтому парадигма Generics здесь не применяется.

Одно из мест, где часто используются ссылки на классы, - это потоковая передача форм. Когда-нибудь просмотрите DFM как текст, и вы увидите, что каждый объект упоминается по имени и классу. (И имя на самом деле необязательно.) Когда средство чтения формы читает первую строку определения объекта, оно получает имя класса. Он ищет его в таблице поиска и извлекает ссылку на класс и использует эту ссылку на класс для вызова переопределения этого класса TComponent.Create(AOwner: TComponent), чтобы он мог создать экземпляр объекта правильного типа, а затем начинает применять свойства, описанные в DFM. Это то, что вам покупают ссылки на классы, и этого нельзя сделать с помощью дженериков.

person Mason Wheeler    schedule 30.07.2010

Я также использовал бы метакласс всякий раз, когда мне нужно иметь возможность создать фабрику, которая может создавать не только один жестко запрограммированный класс, но и любой класс, унаследованный от моего базового класса.

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

Конкретный пример места, где я видел, как это хорошо используется, - это компоненты JVCL JvDocking, где компонент «стиля стыковки» предоставляет информацию метакласса базовому набору компонентов стыковки, так что когда пользователь перетаскивает свою мышь и закрепляет клиентскую форму в форма узла стыковки, формы "узел вкладки" и "узел соединения", которые показывают панели захвата (внешний вид похож на строку заголовка обычного отстыкованного окна), могут относиться к классу подключаемого модуля, определяемому пользователем, который обеспечивает настраиваемый внешний вид и настраиваемые функциональные возможности среды выполнения на основе подключаемого модуля.

person Warren P    schedule 30.07.2010

В некоторых моих приложениях у меня есть механизм, который соединяет классы с формами, способными редактировать экземпляры одного или нескольких из этих классов. У меня есть центральный список, в котором хранятся эти пары: ссылка на класс и ссылка на класс формы. Таким образом, когда у меня есть экземпляр класса, я могу найти соответствующий класс формы, создать из него форму и позволить ей редактировать экземпляр.

Конечно, это также может быть реализовано с помощью метода класса, возвращающего соответствующий класс формы, но для этого потребуется, чтобы класс формы был известен классу. Мой подход делает систему более модульной. Форма должна знать класс, но не наоборот. Это может быть ключевым моментом, когда вы не можете изменить классы.

person Uwe Raabe    schedule 30.07.2010