为什么在 'overloaded' 方法中将参数类型从 'const' 切换到 'var' 时无法传递 'Child' class 实例

Why I can't pass 'Child' class instance when switching parameter type from 'const' to 'var' in 'overloaded' method

MCVE:

在 [=49] 的重载方法 Train 中将参数类型从 const 切换为 varout 时,以下代码不会编译错误=] TAnimalTrainer

但如果未指定,它会编译。

[dcc32 Error] Project14.dpr(41): E2250 There is no overloaded version of 'Train' that can be called with these arguments

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.

找到解决方法:

问题:这是编译器中的错误吗?

Is this a bug in the compiler?

不,不是。

虽然您已经在重载方法的上下文中发现了这一点,但重载掩盖了真正的问题。如果我们移除过载,将更容易理解问题。

因此,为此,考虑这个程序:

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 参数,因此被拒绝。此时,没有剩余的重载方法,因此出现错误消息。

不匹配 var 参数的基本问题是您最终可能在调用变量中使用错误的类型。

您可以通过使用 absolute 关键字来欺骗编译器 - 它允许您声明共享相同 space 的不同类型的变量 - 并模拟如果编译器允许您会发生什么使用这样的结构。

考虑以下示例

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

DogCat 变量共享同一个地址 space,所以当你调用 Train(Cat) 作为结果时,你将得到 TCat 你的实例可以通过 CatDog 变量使用。基本上,您最终会在 TDog 变量中得到 TCat 个实例。

显然,当您调用 Dog.Bark 时,您应该得到 Bark 作为输出而不是 MeowMeowTCat中的第一个方法,就像BarkTDog中的第一个方法一样,当通过TCat解析Bark地址时虚方法 table 它将找到 Meow 方法。由于这两种方法具有相同的签名,如果您认为错误的输出是好的,那么一切都很好。

现在,如果您尝试调用 Dog.Fetch,应用程序将因 AV 而崩溃。 TCat class 中的相应地址没有匹配的方法,你基本上是在调用内存中一些未初始化的地方而不是正确的方法。

这解释了为什么 varout 参数类型必须与调用方变量类型匹配。

至于为什么可以将TDogTCat作为TAnimalconst或值参数传递。 TDogTCat 都继承自 TAnimal 并且 TAnimal 实例可以做什么,TDogTCat 都支持它。它们可以覆盖特定的行为,因此您的猫可以 运行 与您的狗不同,但无论您做什么,它都是明确定义的。您不能以 运行 一些不存在的代码作为结束。

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

当然,如果 TAnimal 实际上是 TDog,这并不妨碍您测试特定的 class 并使用类型转换调用 Fetch

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;