Построение объекта из ссылки на класс - PullRequest
3 голосов
/ 21 января 2009

У меня есть метод, который создает объект, вызывает метод Execute и освобождает объект. Тип объекта определяется потомком TClass, переданным в метод. Обратите внимание, что речь идет о Delphi для Win32, а не .NET.

Редактировать: я должен отметить, что это Delphi 2006, так как в ответах ниже было отмечено, что в будущих версиях вызов NewInstance может не потребоваться. В моем случае, однако, это требуется. Таким образом, я бы предположил, что ответ на мой вопрос (это безопасно? И имеет ли CreateForm () потенциальную утечку) нужно будет ответить, исходя из того, что это Delphi 2006

Правка № 2: кажется, что решения, данные для D2007 и D2009, на самом деле работают для D2006. Должно быть, я приобрел привычку "NewInstance" из более ранней версии Delphi ...

function TPageClassFactory.TryExecute(ScrnClass: TCustomPageClass): boolean;
//TCustomPageClass = class of TCustomPage
var
  ScrnObj: TCustomPage; //TCustomPage defines an abstract Execute() method
begin
  Result := FALSE; //default
  ScrnObj := TCustomPage(ScrnClass.NewInstance); //instantiate
  try
    ScrnObj.Create(Self);  //NB: Create() and Execute() are *virtual* methods
    ScrnObj.Execute;       
  finally
    FreeAndNil(ScrnObj);
  end;
  Result := TRUE;
end;

Я хочу знать, безопасно ли это - что здесь произойдет, если Create () вызовет исключение?

Глядя на похожий пример из Forms.pas.TApplication.CreateForm (), был применен другой подход к обработке исключений (я вырезал ненужные биты ниже):

procedure TApplication.CreateForm(InstanceClass: TComponentClass; var Reference);
var
  Instance: TComponent;
begin
  Instance := TComponent(InstanceClass.NewInstance);
  TComponent(Reference) := Instance;
  try
    Instance.Create(Self);
  except
    TComponent(Reference) := nil;
    raise;
  end;
end;

Означает ли это, что в методе Forms.pas происходит утечка памяти при возникновении исключения в методе Create ()? Насколько я понимаю, InstanceClass.NewInstance выделяет память, поэтому в этом случае память не освобождается / освобождается / освобождается?

Ответы [ 4 ]

12 голосов
/ 21 января 2009

Вы должны поместить создание из блока try finally.

Но лучшее решение:

type 
  TMyClass = class ()
  public
    constructor Create(...); virtual;
    function Execute: Boolean; virtual;
  end;
  TMyClassClass = class of TMyClass;


procedure CreateExecute(const AClass: TMyClassClass): Boolean;
var
  theclass : TMyClass;
begin
  theclass := AClass.Create;
  try
    Result := theclass.Execute;
  finally
    theclass.Free;
  end;
end;
4 голосов
/ 22 января 2009

В комментариях было поднято несколько вопросов, которые я хотел бы уточнить.

Первый - это продолжающийся миф о том, что конструктор должен быть виртуальным. Это не . Рассмотрим этот пример:

type
  TBase = class
    constructor Create(x: Integer);
  end;
  TDerived = class(TBase)
    field: string;
  end;
  TMetaclass = class of TBase;

var
  instance: TBase;
  desiredClass: TMetaclass;
begin
  desiredClass := TDerived;
  instance := desiredClass.Create(23);
  Assert(instance.ClassName = 'TDerived');
  Assert(instance is TDerived);
  Assert(instance.field = '');
end;

Созданный объект будет полноценным экземпляром класса TDerived. Будет выделено достаточно памяти для хранения строкового поля, которого не было в базовом классе.

Есть два условия, которые должны быть выполнены, прежде чем вам понадобится виртуальный конструктор:

  1. Конструктор будет вызываться виртуально. То есть у вас будет переменная типа метакласса базового класса, и она будет содержать значение производного класса, и вы вызовете конструктор для этой переменной. Это продемонстрировано в коде выше. Если все ваши вызовы конструктора находятся непосредственно в самих именах классов (т. Е. TDerived.Create(23)), то ничего нельзя получить от виртуальных методов.
  2. Подкласс базового класса должен будет переопределить конструктор, чтобы обеспечить специфическую для класса инициализацию. Если все потомки используют одну и ту же конструкцию и различаются только другими методами, десять не нужно делать конструктор виртуальным.

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

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


Вторым является вопрос о том, должен ли NewInstance вызываться вместо конструктора или в дополнение к нему. Я думаю, что другие комментарии уже установили, что это не имеет ничего общего с совместимостью со старыми версиями Delphi. Все версии поддерживают вызов конструкторов для ссылок на классы без необходимости NewInstace. Скорее путаница возникает из-за того, что вы смотрите на TApplication.CreateForm и рассматриваете его как пример того, как все должно быть сделано. Это ошибка.

CreateForm вызывает NewInstance перед вызовом конструктора, поскольку основная причина существования CreateForm заключается в том, чтобы убедиться, что глобальная переменная формы, которую объявляет IDE, действительна во время собственных обработчиков событий формы, включая OnCreate, который работает как часть конструктора. Если бы метод CreateForm выполнил обычный шаблон построения, тогда глобальная переменная формы еще не имела бы допустимого значения. Вот что вы могли ожидать увидеть:

TComponent(Reference) := InstanceClass.Create(Application);

Просто и очевидно, но это не сработает. Reference не будет присвоено значение до тех пор, пока после не вернется конструктор, то есть спустя много времени после того, как форма вызовет некоторые события. Если вы следуете хорошей практике программирования и никогда не ссылаетесь на эту переменную из самого класса формы, то вы никогда не заметите. Но если вы будете следовать инструкциям документации, написанным для неопытной аудитории, то вы будете ссылаться на глобальную переменную формы из собственных методов формы, поэтому метод CreateForm делает то, что он может сделать уверен, что он назначен вовремя.

Для этого используется двухступенчатая методика строительства. Сначала выделите память и назначьте ссылку на глобальную переменную:

Instance := TComponent(InstanceClass.NewInstance);
TComponent(Reference) := Instance;

Затем вызовите конструктор экземпляра, передав объект TApplication в качестве владельца:

Instance.Create(Self);

По моему мнению , CreateForm следует вызывать ровно один раз в любой программе. Я бы предпочел ноль раз, но у него есть побочный эффект определения Application.MainForm, что важно для других аспектов программы Delphi.


В-третьих, понятие, что для объекта необычно вызывать конструктор для себя.

На самом деле, это происходит все время . Каждый раз, когда вы вызываете унаследованный конструктор, вы вызываете конструктор для уже существующего объекта. Унаследованный конструктор не выделяет новый объект. Аналогично, в VCL есть несколько примеров ненаследуемых вызовов конструкторов. TCustomForm.Create делегирует большую часть задач построения конструктору CreateNew.

2 голосов
/ 21 января 2009

Ваш вопрос об утечке памяти, когда Create () вызывает исключение: вы должны попробовать это сами. Я только что сделал на Delphi 2007, и с вашим кодом FastMM4 показывает диалоговое окно с ошибкой о попытке вызова виртуального метода для уже освобожденного объекта, а именно Destroy (). Таким образом, исключение в Create уже приведет к вызову деструктора и освобождению памяти, поэтому ваш код на самом деле неверен. Придерживайтесь идиомы, используемой в ответе Gamecat , и все должно работать.

Edit:

Я только что попробовал на Delphi 4, и поведение такое же. Тестовый код:

type
  TCrashComp = class(TComponent)
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  end;

constructor TCrashComp.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  raise Exception.Create('foo');
end;

destructor TCrashComp.Destroy;
begin
  Beep;
  inherited Destroy;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  C: TComponent;
begin
  C := TComponent(TCrashComp.NewInstance);
  try
    C.Create(nil);
    C.Tag := 42;
  finally
    C.Free;
  end;
end;

С FastMM4 блок Free in finally дает ту же ошибку, потому что C уже освобожден. Однако при завершении работы приложения исключение и строка исключения сообщаются как утечки памяти. Это, однако, не проблема с кодом, а со временем выполнения.

2 голосов
/ 21 января 2009

Редактировать:

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

Заметьте, Create вызывал Destroy при неудаче столько, сколько я себя помню. Это не должно быть после того, как я думаю.

Код будет:

procedure TPageClassFactory.TryExecute(ScrnClass: TCustomPageClass);
var
  ScrnObj: TCustomPage;
begin
  ScrnObj := ScrnClass.Create(Self);  // Exception here calls the destructor
  try
    ScrnObj.Execute; // Exception here means you need to free manually      
  finally
    FreeAndNil(ScrnObj); // Be free!
  end;
end;

Я удалил результат, возвращенный исходной функцией, поскольку он никогда не может быть ложным, только «неназначенный» (исключение) или «истина». В конце концов, вы можете получить исключение, прежде чем присваивать результат false. ;)

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