Почему я не могу передать экземпляр класса 'Child' при переключении типа параметра с 'const' на 'var' в методе 'перегружен' - PullRequest
5 голосов
/ 23 марта 2019

MCVE:

Следующий код не компилируется с ошибкой при переключении типа параметра с const на var или out в перегруженном методе Train класса TAnimalTrainer

но он компилируется, если не указан.

[Ошибка dcc32] Project14.dpr (41): E2250 Перегруженная версия отсутствует «Поезда», который можно вызвать с этими аргументами

program Project14;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils;

type    
  TAnimal = class
  private
  FName: string;   
  end;

  TDog = class(TAnimal)
  public
    constructor Create(Name: string);
  end;

  TAnimalTrainer = record // class or record
  public
    procedure Train({const}var aA: TAnimal); overload; // class method or not
    procedure Train(const aName: string); overload;
  end;

{ TAnimalTrainer }

procedure TAnimalTrainer.Train(const aName: string);
var
  Dog: TDog;
begin
  Dog := nil;
  try
    Dog := TDog.Create(aName);  
    Train(Dog); // error here
  finally
    Dog.Free;
  end;
end;

procedure TAnimalTrainer.Train(var aA: TAnimal);
begin
  aA := nil;
end;

{ TDog }

constructor TDog.Create(Name: string);
begin
  FName := Name;
end;



begin
  try       
    { TODO -oUser -cConsole Main : Insert code here }
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

Найдены обходные пути:

  • Опустить var.
  • Приведите локальную переменную к TAnimal(Dog)
  • палка с const.

Вопрос: Это ошибка в компиляторе?

Ответы [ 2 ]

8 голосов
/ 23 марта 2019

Это ошибка в компиляторе?

Нет, это не так.

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

Итак, для этого рассмотрим следующую программу:

type
  TAnimal = class
  end;

  TDog = class(TAnimal)
  end;

procedure GetAnimal(var AAnimal: TAnimal);
begin
  AAnimal := TAnimal.Create;
end;

var
  Dog: TDog;

begin
  GetAnimal(Dog);
end.

Не удается скомпилировать при вызове GetAnimal с этой ошибкой:

[dcc32 Error]: E2033 Types of actual and formal var parameters must be identical

Почему компилятор отклоняет это? Ну, представь, если бы он принял это. Если это так, то при возврате GetAnimal переменная Dog будет ссылаться на объект, который не является TDog.

Чтобы увидеть это, измените тело программы так:

GetAnimal(TAnimal(Dog));
Writeln(Dog.InheritsFrom(TDog));

Когда вы это делаете, программа компилируется, но вывод

FALSE

В контексте вашей программы компилятор сталкивается с некоторыми перегрузками. Как мы видели в этом примере, компилятор не может принять передачу переменной TDog параметру TAnimal var, поэтому он отклоняет эту перегрузку. Он знает, что не может передать переменную TDog параметру string, поэтому он отклоняется. В этот момент не осталось перегруженных методов, поэтому появляется сообщение об ошибке.

3 голосов
/ 24 марта 2019

Основная проблема с несоответствующими параметрами var - это вероятность того, что в вызывающей переменной вы получите неправильный тип.

Вы можете обмануть компилятор, используя ключевое слово absolute, которое позволяет вам объявлять переменные различного типа с одинаковым пространством, и моделировать, что произойдет, если компилятор позволит вам использовать такую ​​конструкцию.

Рассмотрим следующий пример

uses
  System.SysUtils;

type
  TAnimal = class
  public
    procedure Run; virtual;
  end;

  TDog = class(TAnimal)
  public
    procedure Bark; virtual;
    procedure Fetch; virtual;
  end;

  TCat = class(TAnimal)
  public
    procedure Meow; virtual;
  end;

procedure TAnimal.Run;
begin
  Writeln('Run');
end;

procedure TDog.Bark;
begin
  Writeln('Bark');
end;

procedure TDog.Fetch;
begin
  Writeln('Fetch');
end;

procedure TCat.Meow;
begin
  Writeln('Meow');
end;

procedure Move(const aA: TAnimal);
begin
  aA.Run;
end;

procedure Train(var aA: TAnimal);
begin
  aA := TCat.Create;
end;

var
  Dog: TDog;
  Cat: TAnimal absolute Dog;
begin
  try
    // we cannot use Dog here, because compiler would refuse to compile such code
    // Cat is TAnimal and compiler allows to pass it
    // since Dog and Cat variables share same address space that is
    // equivalent of calling Train(Dog);
    Train(Cat); 

    Move(Cat);
    Dog.Bark;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

Если вы запустите приведенный выше код, вы получите следующие выходные данные

Run
Meow

Dog и Cat переменные разделяют одно и то же адресное пространствопоэтому, когда вы в результате вызовете Train(Cat), вы получите экземпляр TCat, который вы можете использовать либо через переменную Cat, либо Dog.По сути, вы получите TCat экземпляр внутри переменной TDog.

Очевидно, что когда вы вызываете Dog.Bark, вы должны получить Bark в качестве вывода, а не Meow.Meow - первый метод в TCat, точно так же, как Bark - первый метод в TDog, и при разрешении адреса Bark через таблицу виртуальных методов TCat он найдет метод Meow.Поскольку оба метода имеют одинаковую сигнатуру, все в порядке, если вы считаете неправильный вывод правильным.

Теперь, если вы попытаетесь вызвать Dog.Fetch, приложение завершит работу с AV.В соответствующем адресе класса TCat нет подходящих методов, и вы в основном вызываете неинициализированное место в памяти вместо правильного метода.

Это объясняет, почему типы параметров var или out должны соответствовать вызывающей стороне.тип переменной.

Почему вы можете передать TDog или TCat как TAnimal const или параметр-значение.И TDog, и TCat наследуются от TAnimal, и все, что вы можете сделать с TAnimal экземпляром, оба TDog и TCat поддерживают его.Они могут отменять определенное поведение, поэтому ваша кошка может работать не так, как ваша собака, но что бы вы ни делали, это четко определено.Вы не можете в конечном итоге выполнить какой-то несуществующий код.

procedure Move(const aA: TAnimal);
begin
  aA.Run;
  aA.Fetch; // this will fail to compile - there is no Fetch method in TAnimal class
end;

Конечно, это не мешает вам тестировать определенный класс и использовать приведение типов для вызова Fetch, если TAnimal на самом деле TDog,

procedure Move(const aA: TAnimal);
begin
  aA.Run;
  if aA is TDog then TDog(aA).Fetch;
end;

Однако, если вы злоупотребляете приведением и приведением типов без проверки, является ли конкретная переменная на самом деле TDog экземпляром, вы снова подключитесь к AV.

procedure Move(const aA: TAnimal);
begin
  aA.Run;
  TDog(aA).Fetch;
end;
...