Delphi: Понимание конструкторов

31

Я хочу понять

  • виртуальный
  • переопределить
  • перегрузка
  • реинтродукции

при применении к конструкторам объектов. Каждый раз, когда я произвольно добавляю ключевые слова до тех пор, пока компилятор не закроется - и (после 12 лет разработки с Delphi), я бы лучше знал, что я делаю, а не пытался случайным образом.

Учитывая гипотетический набор объектов:

TComputer = class(TObject)
public
    constructor Create(Cup: Integer); virtual;
end;

TCellPhone = class(TComputer)
public
    constructor Create(Cup: Integer; Teapot: string); virtual;
end;

TiPhone = class(TCellPhone)
public
    constructor Create(Cup: Integer); override;
    constructor Create(Cup: Integer; Teapot: string); override;
end;

То, как я хочу, чтобы они вели себя, вероятно, очевидно из деклараций, но:

  • TComputer имеет простой конструктор, и потомки могут его переопределить
  • TCellPhone имеет альтернативный конструктор, и потомки могут его переопределить
  • TiPhone переопределяет оба конструктора, вызывая унаследованную версию каждого

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

Я хочу понять, почему. Тогда это будет очевидно.

См. также

Изменить: Я также хочу получить некоторые рассуждения о порядке virtual, override, overload, reintroduce. Потому что при попытке всех комбинаций ключевых слов число комбинаций взрывается:

  • виртуальный; перегрузки;
  • виртуальный; переопределить;
  • переопределение; перегрузки;
  • переопределение; виртуальный;
  • виртуальный; переопределить; перегрузки;
  • виртуальный; перегрузки; переопределить;
  • перегрузки; виртуальный; переопределить;
  • переопределение; виртуальный; перегрузки;
  • переопределение; перегрузки; виртуальный;
  • перегрузки; переопределить; виртуальный;
  • и т.д.

Изменить 2: Я думаю, мы должны начать с "является ли возможна иерархия объектов? Если нет, почему бы и нет? Например, принципиально неверно иметь конструктор от предка?

TComputer = class(TObject)
public
    constructor Create(Cup: Integer); virtual;
end;

TCellPhone = class(TComputer)
public
    constructor Create(Cup: Integer; Teapot: string); virtual;
end;

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


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

Теперь эти объявления не компилируются:

//Method Create hides virtual method of base type TComputer:
TCellPhone = class(TComputer)
   constructor Create(Cup: Integer; Teapot: string);  virtual;

//Method Create hides virtual method of base type TCellPhone:
TiPhone = class(TCellPhone)
public
   constructor Create(Cup: Integer); override;
   constructor Create(Cup: Integer; Teapot: string); overload;  <--------
end;

Итак, сначала я попытаюсь установить TCellPhone. я начну с случайного добавления ключевого слова overload (я знаю, что не хочу reintroduce, потому что это скроет другой конструктор, который я не хочу):

TCellPhone = class(TComputer)
public
   constructor Create(Cup: Integer; Teapot: string); virtual; overload;
end;

Но это не удается: Field definition not allowed after methods or properties.

Я знаю по опыту, что даже если у меня нет поля после метода или свойства, если я отменил порядок ключевых слов virtual и overload: Delphi закроется:

TCellPhone = class(TComputer)
public
   constructor Create(Cup: Integer; Teapot: string); overload; virtual; 
end;

Но я все еще получаю ошибку:

Метод "Создать" скрывает виртуальный метод базового типа "TComputer"

Итак, я пытаюсь удалить оба ключевых слова:

TCellPhone = class(TComputer)
public
   constructor Create(Cup: Integer; Teapot: string);
end;

Но я все еще получаю ошибку:

Метод "Создать" скрывает виртуальный метод базового типа "TComputer"

Итак, я схожу, чтобы теперь попробовать reintroduce:

TCellPhone = class(TComputer)
public
   constructor Create(Cup: Integer; Teapot: string); reintroduce;
end;

И теперь TCellPhone компилируется, но для TiPhone это намного хуже:

TiPhone = class(TCellPhone)
public
   constructor Create(Cup: Integer); override; <-----cannot override a static method
   constructor Create(Cup: Integer; Teapot: string); override; <-----cannot override a static method
end;

Оба жалуются, что я не могу их переопределить, поэтому я удаляю ключевое слово override:

TiPhone = class(TCellPhone)
public
   constructor Create(Cup: Integer);
   constructor Create(Cup: Integer; Teapot: string);
end;

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

TiPhone = class(TCellPhone)
public
   constructor Create(Cup: Integer); overload;
   constructor Create(Cup: Integer; Teapot: string); overload;
end;

В разделе interface все хорошо. К сожалению, мои реализации не будут работать. Мой единственный конструктор параметров TiPhone не может вызвать унаследованный конструктор:

constructor TiPhone.Create(Cup: Integer);
begin
    inherited Create(Cup); <---- Not enough actual parameters
end;
  • 0
    Из справки Delphi 9: Объявления методов могут включать специальные директивы, которые не используются с другими функциями или процедурами. Директивы должны появляться только в объявлении класса, а не в определяющем объявлении, и всегда должны быть перечислены в следующем порядке: reintroduce ; overload ; переплет ; соглашение о вызовах ; abstract ; предупреждение о том, что привязка является virtual , dynamic или override ; соглашение о вызовах - это register , pascal , cdecl , stdcall или safecall ; и предупреждение - это platform , deprecated или library .
  • 0
    Delphi 5 все еще поставляется с отличным печатным языковым справочником, подробно объясняющим эти заявления.
Показать ещё 1 комментарий
Теги:
constructor
delphi-5
constructor-chaining

4 ответа

15
Лучший ответ

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

  • В TCellPhone должно быть предупреждение, что его конструктор скрывает метод базового класса. Это связано с тем, что метод базового класса является виртуальным, и компилятор беспокоится о том, что вы вводите новый метод с тем же именем без переопределения метода базового класса. Неважно, что подписи различаются. Если ваше намерение действительно скрывает метод базового класса, тогда вам нужно использовать reintroduce в декларации потомков, как показало одно из ваших слепых догадок. Единственная цель этой директивы - подавить предупреждение; он не влияет на поведение во время выполнения.

    Игнорируя, что произойдет с TIPhone позже, следующее объявление TCellPhone - это то, что вы хотите. Он скрывает метод предков, но вы также хотите, чтобы он был виртуальным. Он не наследует виртуальность метода предка, потому что это два совершенно разных метода, которые просто имеют одно и то же имя. Поэтому вам нужно использовать virtual и в новой декларации.

    TCellPhone = class(TComputer)
    public
      constructor Create(Cup: Integer; Teapot: string); reintroduce; virtual;
    end;
    

    Конструктор базового класса TComputer.Create также скрывает метод своего предка TObject.Create, но поскольку метод в TObject не является виртуальным, компилятор не предупреждает об этом. Скрытие не виртуальных методов происходит все время и вообще не примечательно.

  • Вы должны получить сообщение об ошибке в TIPhone, потому что больше нельзя использовать конструктор с одним аргументом. Вы спрятали его в TCellPhone. Поскольку вы хотите иметь два конструктора, reintroduce явно не был правильным выбором для использования ранее. Вы не хотите скрывать конструктор базового класса; вы хотите увеличить его с помощью другого конструктора.

    Поскольку вы хотите, чтобы оба конструктора имели одинаковое имя, вам нужно использовать директиву overload. Эта директива должна использоваться в всех исходных объявлениях - при первом вводе каждой отдельной подписи последующие объявления в потомках. Я думал, что это необходимо для всех объявлений (даже базового класса), и это не помешает сделать это, но я думаю, это не требуется. Итак, ваши объявления должны выглядеть так:

    TComputer = class(TObject)
    public
      constructor Create(Cup: Integer);
        overload; // Allow descendants to add more constructors named Create.
        virtual;  // Allow descendants to re-implement this constructor.
    end;
    
    TCellPhone = class(TComputer)
    public
      constructor Create(Cup: Integer; Teapot: string);
        overload; // Add another method named Create.
        virtual;  // Allow descendants to re-implement this constructor.
    end;
    
    TiPhone = class(TCellPhone)
    public
      constructor Create(Cup: Integer);
        override; // Re-implement the ancestor Create(Integer).
      constructor Create(Cup: Integer; Teapot: string);
        override; // Re-implement the ancestor Create(Integer, string).
    end;
    

Современная документация рассказывает, в каком порядке все должно идти:

вновь ввести; перегрузки; связывание; вызывающая конвенция; абстрактный; предупреждение

где привязка виртуальная, динамическая или переопределяет; вызов зарегистрировать, pascal, cdecl, stdcall или safecall; и предупреждение платформа, устаревшая или библиотека.

Это шесть разных категорий, но, по моему опыту, редко бывает более трех в любой декларации. (Например, функции, для которых требуются указанные соглашения вызова, вероятно, не являются методами, поэтому они не могут быть виртуальными.) Я никогда не помню порядок; Я никогда не видел, чтобы это было зарегистрировано до сегодняшнего дня. Вместо этого я считаю более полезным запомнить каждую директивную цель. Когда вы помните, какие директивы вам нужны для разных задач, вы получите всего два или три, а затем довольно просто поэкспериментировать, чтобы получить правильный порядок. Компилятор может принимать несколько заказов, но не волнуйтесь - порядок не имеет значения при определении значения. Любое упорядочение, которое принимает компилятор, будет иметь то же значение, что и любое другое (за исключением вызовов условных обозначений, если вы упомянете более одного из них, учитывается только последний, поэтому не делайте этого).

Итак, тогда вам просто нужно запомнить цель каждой директивы и подумать, какие из них не имеют никакого смысла. Например, вы не можете использовать reintroduce и override в то же время, потому что они имеют противоположные значения. И вы не можете использовать virtual и override вместе, потому что один означает другой.

Если у вас много нагромождений директив, вы всегда можете вырезать overload из картинки, когда вы будете работать с остальными директивами, которые вам нужны. Дайте вашим методам разные имена, определите, какие из других директив им нужны сами, а затем добавьте overload назад, пока вы снова дадите им все те же имена.

5

Заметьте, что у меня нет Delphi 5, поэтому я основываю свои ответы на последней версии Delphi XE. Я не думаю, что это действительно будет иметь значение здесь, но если это произойдет, вас предупредили.:)

В основном это основан на http://docwiki.embarcadero.com/RADStudio/en/Methods, который является текущей документацией о том, как работают методы. Возможно, у вашего файла справки Delphi 5 есть что-то похожее.

Во-первых, виртуальный конструктор может не иметь особого смысла здесь. Есть несколько случаев, когда вы этого хотите, но это, вероятно, не одно. Взгляните на http://docwiki.embarcadero.com/RADStudio/en/Class_References для ситутации, где вам нужен виртуальный конструктор - если вы всегда знаете тип своих объектов при кодировании, однако, вы этого не делаете.

Проблема, которую вы затем получаете в своем конструкторе с 1 параметром, состоит в том, что ваш родительский класс не имеет самого конструктора с 1 параметром - унаследованные конструкторы не отображаются. Вы не можете использовать inherited для перехода на несколько уровней в иерархии, вы можете только вызвать своего непосредственного родителя. Вам нужно будет вызвать конструктор с двумя параметрами с некоторым значением по умолчанию или добавить конструктор с 1 параметром в TCellPhone.

В целом, четыре ключевых слова имеют следующие значения:

  • virtual - Отметьте это как функцию, где вам понадобится диспетчеризация во время выполнения (допускает полиморфное поведение). Это только для первоначального определения, а не при переопределении в подклассах.
  • override - предоставить новую реализацию для виртуального метода.
  • overload - Отметьте функцию с тем же именем, что и другая функция, но другой список параметров.
  • reintroduce - Расскажите компилятору, что вы на самом деле намеревались скрыть виртуальный метод, вместо того, чтобы просто забыть предоставить override.

Требуемое упорядочение подробно описано в документации:

Объявления метода могут включать специальные директивы, которые не используются с другими функциями или процедурами. Директивы должны появиться в классе декларации, а не в определении декларации и всегда должны быть перечисленные в следующем порядке:

вновь; перегрузки; связывание; вызывающая конвенция; Абстрактные; предупреждение

где привязка является виртуальной, динамической или переопределить; вызывающая конвенция register, pascal, cdecl, stdcall или директива SafeCall; и предупреждение - это платформа, устаревшие или библиотеки.

  • 0
    Почему простой пример, использующий только первые два класса, все еще не работает? Компилятор жалуется, что Create(int, string) скрывает базу Create(int) . У него другая подпись, как он это скрывает?
  • 0
    @ Ян, у них одинаковое имя. Если вам нужны две вещи с одним и тем же именем, но разными сигнатурами, вы перегружаетесь , и в обоих объявлениях должна использоваться директива overload .
Показать ещё 3 комментария
2

Это рабочая реализация требуемых определений:

program OnConstructors;

{$APPTYPE CONSOLE}

uses
  SysUtils;

type

TComputer = class(TObject)
public
    constructor Create(Cup: Integer); virtual;
end;

TCellPhone = class(TComputer)
public
    constructor Create(Cup: Integer; Teapot: string); reintroduce; overload; virtual;
end;

TiPhone = class(TCellPhone)
public
    constructor Create(Cup: Integer); overload; override;
    constructor Create(Cup: Integer; Teapot: string); override;
end;

{ TComputer }

constructor TComputer.Create(Cup: Integer);
begin
  Writeln('Computer: cup = ', Cup);
end;

{ TCellPhone }

constructor TCellPhone.Create(Cup: Integer; Teapot: string);
begin
  inherited Create(Cup);
  Writeln('Cellphone: teapot = ', Teapot);
end;

{ TiPhone }

constructor TiPhone.Create(Cup: Integer);
begin
  inherited Create(Cup);
  Writeln('iPhone: cup = ', Cup);
end;

constructor TiPhone.Create(Cup: Integer; Teapot: string);
begin
  inherited;
  Writeln('iPhone: teapot = ', Teapot);
end;

var
  C: TComputer;

begin

  C := TComputer.Create(1);
  Writeln; FreeAndNil(C);

  C := TCellPhone.Create(2);
  Writeln; FreeAndNil(C);
  C := TCellPhone.Create(3, 'kettle');
  Writeln; FreeAndNil(C);

  C := TiPhone.Create(4);
  Writeln; FreeAndNil(C);
  C := TiPhone.Create(5, 'iPot');

  Readln; FreeAndNil(C);

  end.

с результатами:

Computer: cup = 1

Computer: cup = 2

Computer: cup = 3
Cellphone: teapot = kettle

Computer: cup = 4
iPhone: cup = 4

Computer: cup = 5
Cellphone: teapot = iPot
iPhone: teapot = iPot

Первая часть соответствует этому. Определение двух конструкторов TiPhone выполняется следующим образом:

  • Первый конструктор перегружает один из двух конструкторов, унаследованных и переопределяющих свой родной брат. Для этого используйте overload; override, чтобы перегрузить TCellPhone один, переопределив другой конструктор.
  • Для этого второй конструктор нуждается в простой override, чтобы переопределить свой родной брат.
0

используйте перегрузку для обоих, это так, как я делаю это, и он работает.

constructor Create; Overload; < - используйте перегрузку здесь

constructor Values; Overload; < - и здесь

Помните, что нельзя использовать одно и то же имя для двух разных конструкторов

Ещё вопросы

Сообщество Overcoder
Наверх
Меню