5.3.4 运算符重载
另一个与记录相关的 Object Pascal
语言特性是运算符重载,即在数据类型上自己定义标准操作(加法、乘法、比较等)的能力。基本思想是你可以实现一个加法运算符(一个特殊的 Add 方法),然后使用 + 符号来调用它。要定义运算符,你需要使用 class operator
关键字的组合。
注解:通过重用现有的保留字,语言设计者成功地做到了对现有代码没有产生影响。他们最近在关键字组合中经常这样做,比如
strict private
、class operator
和class var
。
这里的 class
与类方法有关,这是我们将在更后面的章节中要探讨的概念(在第12章)。在指令之后,你写出运算符的名称,例如 Add:
type
TPointRecord = record
public
class operator Add(A, B: TPointRecord): TPointRecord;
然后使用 + 符号调用 Add
运算符,如你所期望:
var
A, B, C: TPointRecord;
begin
C := A + B;
那么有哪些可用的运算符呢?基本上是语言的整个运算符集,因为你不能定义全新的运算符:
- 强制类型转换运算符:
Implicit
和Explicit
- 一元运算符:Positive, Negative, Inc, Dec, LogicalNot, BitwiseNot,
Trunc, 和 Round - 比较运算符:Equal, NotEqual, GreaterThan, GraterThanOrEqual, LessThan, 和LessThenOrEqual
- 二元运算符:Add, Subtract, Multiply, Divide, IntDivide, Modulus,ShiftLeft, ShiftRight, LogicalAnd, LogicalOr, LogicalXor, BitwiseAnd, BitwiseOr, 和 BitwiseXor.
- 托管记录运算符:Initialize、Finalize、Assign(有关这三个在 Delphi 10.4 中添加的运算符的详细信息,请参见下一节“运算符和自定义托管记录”)
在调用运算符的代码中,你不是使用这些名称,而是使用相应的符号。你仅在定义中使用这些特殊名称,使用 class operator
前缀以避免任何可能的命名冲突。例如,您可以在一条记录中同时使用 Add 方法和 Add 操作符,而不会造成命名冲突。
在定义这些运算符时,你要明确指定参数,然后当“调用”完全匹配参数时才能应用该运算符。要把两个不同类型的值相加,你必须指定两个不同的 Add
操作,因为每个操作数都可以是表达式的第一个或第二个条目。实际上,运算符的定义不提供自动交换的能力。此外,你必须非常精确地指定类型,因为自动类型转换不适用。这往往意味着定义运算符的多个重载版本,并使用不同类型的参数。
另一个需要注意的重要因素是,可以定义两种特殊的数据转换操作符,即 Implicit
和 Explicit
。第一个用于定义隐式类型转换(或静默转换),应该是完美的且不会有损失。第二个,Explicit
,只有在从一个类型的变量向另一个类型进行显式类型转换时才能调用。这两个操作符共同定义了允许在给定数据类型之间进行的类型转换。
请注意,Implicit
和 Explicit
运算符都可以根据函数的返回类型进行重载,而重载方法通常无法做到这一点。实际上,在进行类型转换时,编译器知道预期的结果类型,并可以找出要应用的类型转换操作。例如,我编写了 OperatorsOver
示例,其中定义了记录的一些运算符:
type
TPointRecord = record
private
X, Y: Integer;
public
procedure SetValue(X1, Y1: Integer);
class operator Add(A, B: TPointRecord): TPointRecord;
class operator Explicit(A: TPointRecord): string;
class operator Implicit(X1: Integer): TPointRecord;
end;
以下是记录方法的实现:
class operator TPointRecord.Add(A, B: TPointRecord): TPointRecord;
begin
Result.X := A.X + B.X;
Result.Y := A.Y + B.Y;
end;
class operator TPointRecord.Explicit(A: TPointRecord): string;
begin
Result := Format('(%d:%d)', [A.X, A.Y]);
end;
class operator TPointRecord.Implicit(X1: Integer): TPointRecord;
begin
Result.X := X1;
Result.Y := 10;
end;
使用这样的记录非常简单,你可以编写如下代码:
procedure TForm1.Button1Click(Sender: TObject);
var
A, B, C: TPointRecord;
begin
A.SetValue(10, 10);
B := 30;
C := A + B;
Show(string(C));
end;
第二个赋值(B := 30;
)使用了隐式运算符,由于缺少强制转换,而 Show
调用使用了强制转换符号以激活显式类型转换。此外,Add
运算符并不修改其参数;相反,它返回一个全新的值。
注解:运算符返回新值的事实使得我们更难考虑对类进行运算符重载。如果操作符创建了一个新的临时对象,谁来处理它呢?
运算符重载的背后
这是一个相当高级的简短部分,你可能希望首次阅读时跳过。
从技术上讲,你可以使用运算符的内部限定全称(如 &&op_Addition)来调用运算符, 前缀为&&
,这个技术鲜为人知。例如,你可以将记录和写法的总和重写如下(完整列表请参阅演示):
C := TPointRecord.&&op_Addition(A, B);
尽管我认为只有极少数的情况需要这样做。(定义运算符的全部目的是能够使用比普通方法名或更丑陋的直接调用生成的方法名更友好的表示法。)
实现交换性
假设您想把一个记录与一个整数相加。您可以定义以下操作符(OperatorsOver 示例代码中提供了该操作符,但记录类型略有不同)::
class operator TPointRecord2.Add(A: TPointRecord2; B: Integer): TPointRecord2;
begin
Result.X := A.X + B;
Result.Y := A.Y + B;
end;
注解:我之所以为新类型而不是现有类型定义这个操作符,是因为同一结构已经定义了整数到记录类型的隐式转换,因此我已经可以添加整数和记录,而无需定义特定的操作符。这个问题将在下一节中作进一步解释。
现在你可以合法地将浮点值添加到记录中:
var
A: TPointRecord2;
begin
A.SetValue(10, 20);
A := A + 10;
然而,如果你尝试编写相反的加法:
A := 30 + A;
这将失败并显示错误:
[dcc32 Error] E2015 Operator not applicable to this operand type
实际上,正如我所提到的,对于应用于不同类型变量的运算符来说,交换性不是自动的,而必须由重复调用或调用(如下所示)运算符的另一个版本来明确实现:
class operator TPointRecord2.Add(B: Integer; A: TPointRecord2): TPointRecord2;
begin
Result := A + B; // 实现交换性
end;
隐式转换和类型提升
需要注意的是,调用重载运算符的规则解析与调用方法的传统规则解析不同。在类型自动提升的情况下,一个表达式有可能最终调用不同版本的重载操作符,从而导致调用含糊不清。这就是为什么在编写 Implicit
运算符时需要非常小心的原因。
考虑一下前面示例中的这些表达式:
A := 50;
C := A + 30;
C := 50 + 30;
C := 50 + TPointRecord(30);
它们都是合法的!在第一种情况下,编译器会将 30 转换为正确的记录类型;在第二种情况下,转换发生在赋值之后;在第三种情况下,显式转换会强制对第一个值进行隐式转置,因此执行的加法是记录之间的自定义加法。换句话说,第二个操作的结果与其他两个操作不同,这一点在输出(显示 X 和 Y 值)和这些语句的扩展版本中都有突出显示:
// 输出
(80:20)
(80:10)
(80:20)
// 扩展语句
C := A + TPointRecord(30);
// 即: (50:10) + (30:10)
C := TPointRecord (50 + 30);
// 即: 80 转换为 (80:10)
C := TPointRecord(50) + TPointRecord(30);
// 即: (50:10) + (30:10)
在第一个情况下,编译器将30转换为适当的记录类型,以适应赋值目标。在第二个情况下,由于类型提升,转换发生在赋值之后。而在第三个情况下,显式强制转换在第一个值上执行了隐式转换,因此执行的是记录之间的自定义加法操作。
需要特别注意的是,这种类型提升会导致表达式最终调用不同版本的重载运算符,可能会引发模棱两可的调用。因此,在编写隐式运算符时,需要特别小心。