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

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

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

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

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

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

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.

Да, Collection по-прежнему сможет содержать любого потомка 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, где бы они ни были объявлены.

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

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

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

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

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

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

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

Другие вопросы по тегам