为什么在 '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
切换为 var
或 out
时,以下代码不会编译错误=] 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.
找到解决方法:
- 省略
var
。
- 将局部变量转换为
TAnimal(Dog)
- 坚持
const
。
问题:这是编译器中的错误吗?
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
Dog
和 Cat
变量共享同一个地址 space,所以当你调用 Train(Cat)
作为结果时,你将得到 TCat
你的实例可以通过 Cat
或 Dog
变量使用。基本上,您最终会在 TDog
变量中得到 TCat
个实例。
显然,当您调用 Dog.Bark
时,您应该得到 Bark
作为输出而不是 Meow
。 Meow
是TCat
中的第一个方法,就像Bark
是TDog
中的第一个方法一样,当通过TCat
解析Bark
地址时虚方法 table 它将找到 Meow
方法。由于这两种方法具有相同的签名,如果您认为错误的输出是好的,那么一切都很好。
现在,如果您尝试调用 Dog.Fetch
,应用程序将因 AV 而崩溃。 TCat
class 中的相应地址没有匹配的方法,你基本上是在调用内存中一些未初始化的地方而不是正确的方法。
这解释了为什么 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;
当然,如果 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;
MCVE:
在 [=49] 的重载方法 Train
中将参数类型从 const
切换为 var
或 out
时,以下代码不会编译错误=] 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.
找到解决方法:
- 省略
var
。 - 将局部变量转换为
TAnimal(Dog)
- 坚持
const
。
问题:这是编译器中的错误吗?
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
Dog
和 Cat
变量共享同一个地址 space,所以当你调用 Train(Cat)
作为结果时,你将得到 TCat
你的实例可以通过 Cat
或 Dog
变量使用。基本上,您最终会在 TDog
变量中得到 TCat
个实例。
显然,当您调用 Dog.Bark
时,您应该得到 Bark
作为输出而不是 Meow
。 Meow
是TCat
中的第一个方法,就像Bark
是TDog
中的第一个方法一样,当通过TCat
解析Bark
地址时虚方法 table 它将找到 Meow
方法。由于这两种方法具有相同的签名,如果您认为错误的输出是好的,那么一切都很好。
现在,如果您尝试调用 Dog.Fetch
,应用程序将因 AV 而崩溃。 TCat
class 中的相应地址没有匹配的方法,你基本上是在调用内存中一些未初始化的地方而不是正确的方法。
这解释了为什么 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;
当然,如果 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;