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

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

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

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

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

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

Ответы

Ответ 1

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.

Ответ 2

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

Ответ 3

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

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

Ответ 4

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

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

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

Ответ 5

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

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