Ссылки на классы Delphi ... метаклассы ... когда их использовать - PullRequest
14 голосов
/ 30 июля 2010

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

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

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

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

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

Ответы [ 5 ]

22 голосов
/ 31 июля 2010

MetaClasses и "процедуры класса"

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.

8 голосов
/ 30 июля 2010

Да, 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, где бы они ни были объявлены.

4 голосов
/ 30 июля 2010

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

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

1 голос
/ 30 июля 2010

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

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

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

0 голосов
/ 30 июля 2010

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...