overload - 用于重载 Perl 操作的包
package SomeThing;
use overload
'+' => \&myadd,
'-' => \&mysub;
# etc
...
package main;
$a = SomeThing->new( 57 );
$b = 5 + $a;
...
if (overload::Overloaded $b) {...}
...
$strval = overload::StrVal $b;
此 pragma 允许为类重载 Perl 的操作符。要重载内置函数,请参阅 "perlsub 中的重写内置函数"。
use overload
指令的参数是 (键,值) 对。有关所有合法键的完整列表,请参阅下面的 "可重载操作"。
操作符实现(值)可以是子程序、子程序引用或匿名子程序 - 换句话说,任何在 &{ ... }
调用中合法的代码。指定为字符串的值被解释为方法名。因此
package Number;
use overload
"-" => "minus",
"*=" => \&muas,
'""' => sub { ...; };
声明减法将由 Number
类(或其基类之一)中的 minus()
方法实现,并且 Number::muas()
函数将用于乘法的赋值形式 *=
。它还定义了一个匿名子程序来实现字符串化:每当将一个祝福到 Number
包中的对象用于字符串上下文时,就会调用它(例如,此子程序可以将数字作为罗马数字返回)。
以下 minus()
的示例实现(假设 Number
对象只是对标量的祝福引用)说明了调用约定
package Number;
sub minus {
my ($self, $other, $swap) = @_;
my $result = $$self - $other; # *
$result = -$result if $swap;
ref $result ? $result : bless \$result;
}
# * may recurse once - see table below
在 use overload
指令中指定的所有子程序中传递三个参数(有例外 - 请参见下文,特别是 "nomethod")。
第一个参数是提供重载操作符实现的操作数 - 在这种情况下,是调用其 minus()
方法的对象。
第二个参数是另一个操作数,或者在单目操作符的情况下为 undef
。
第三个参数在两个操作数被交换的情况下(且仅在这种情况下)设置为 TRUE。Perl 可能会这样做以确保第一个参数($self
)是实现重载操作的对象,这与一般的对象调用约定一致。例如,如果 $x
和 $y
是 Number
operation | generates a call to
============|======================
$x - $y | minus($x, $y, '')
$x - 7 | minus($x, 7, '')
7 - $x | minus($x, 7, 1)
Perl 还可以使用 minus()
来实现 use overload
指令中未指定的其他操作符,根据后面描述的 "魔法自动生成" 规则。例如,上面的 use overload
没有为任何操作符 --
、neg
(单目减法的重载键)或 -=
声明任何子程序。因此
operation | generates a call to
============|======================
-$x | minus($x, 0, 1)
$x-- | minus($x, 1, undef)
$x -= 3 | minus($x, 3, undef)
注意 undef
:当自动生成导致使用不改变其任何操作数的标准操作符的方法来实现改变操作数的操作符(“变异器”:这里,--
和 -=
)时,Perl 将 undef
作为第三个参数传递。这仍然评估为 FALSE,与操作数未被交换的事实一致,但它给了子程序一个机会在这些情况下改变其行为。
在以上所有示例中,minus()
只需要返回减法的结果:Perl 会负责将结果赋值给 $x
。实际上,此类方法不应该修改其操作数,即使 undef
被作为第三个参数传递(请参见 "可重载操作")。
++
和 --
的实现并非如此:它们应该修改其操作数。--
的适当实现可能如下所示
use overload '--' => "decr",
# ...
sub decr { --${$_[0]}; }
如果启用了“按位”功能(参见 feature),则会将第五个 TRUE 参数传递给处理 &
、|
、^
和 ~
的子例程。这表示调用方期望数值行为。第四个参数将为 undef
,因为该位置 ($_[3]
) 预留给 "nomethod" 使用。
术语“数学魔术”描述了数学运算符的重载实现。数学魔术运算会引发一个问题。考虑以下代码
$a = $b;
--$a;
如果 $a
和 $b
是标量,那么在执行这些语句后
$a == $b - 1
但是,对象是对祝福数据的引用,因此如果 $a
和 $b
是对象,那么赋值 $a = $b
仅复制引用,使 $a
和 $b
引用相同对象数据。因此,人们可能会期望操作 --$a
也会递减 $b
以及 $a
。但是,这与我们期望数学运算符的工作方式不一致。
Perl 通过在调用定义为实现变异器 (--
、+=
等) 的方法之前透明地调用复制构造函数来解决此困境。在上面的示例中,当 Perl 遇到递减语句时,它会复制 $a
中的对象数据,并将对复制数据的引用分配给 $a
。只有这样,它才会调用 decr()
,该方法会更改复制的数据,使 $b
保持不变。因此,对象隐喻尽可能地保留,而数学魔术运算仍然根据算术隐喻工作。
注意:前一段描述了当 Perl 根据标量自动生成对象的复制构造函数时会发生什么。对于其他情况,请参见 "复制构造函数"。
可以在 use overload
指令中指定的键的完整列表在哈希 %overload::ops
的值中给出,并用空格隔开。
with_assign => '+ - * / % ** << >> x .',
assign => '+= -= *= /= %= **= <<= >>= x= .=',
num_comparison => '< <= > >= == !=',
'3way_comparison' => '<=> cmp',
str_comparison => 'lt le gt ge eq ne',
binary => '& &= | |= ^ ^= &. &.= |. |.= ^. ^.=',
unary => 'neg ! ~ ~.',
mutators => '++ --',
func => 'atan2 cos sin exp abs log sqrt int',
conversion => 'bool "" 0+ qr',
iterators => '<>',
filetest => '-X',
dereferencing => '${} @{} %{} &{} *{}',
matching => '~~',
special => 'nomethod fallback =',
大多数可重载运算符与这些键一一对应。例外情况(包括此哈希中没有显示的额外可重载运算)包含在后面的注释中。此列表会随着时间的推移而增长。
如果尝试注册上面未找到的运算符,则会发出警告。
not
运算符 not
不是 use overload
的有效键。但是,如果运算符 !
被重载,那么相同的实现将用于 not
(因为这两个运算符仅在优先级上有所不同)。
neg
键 neg
用于一元减号,以将其与二元 -
区分开。
++
, --
假设它们的行为类似于 Perl 的 ++
和 --
,这些运算符的重载实现需要修改其操作数。
递增和递减运算符的前缀和后缀形式没有区别:它们仅在 Perl 在评估表达式时调用关联子例程的点上有所不同。
赋值
+= -= *= /= %= **= <<= >>= x= .=
&= |= ^= &.= |.= ^.=
简单赋值不可重载('='
键用于 "复制构造函数")。Perl 确实有一种方法可以使对对象的赋值执行您想要的任何操作,但这涉及使用 tie(),而不是 overload - 请参见 "perlfunc 中的 tie" 和下面的 "COOKBOOK" 示例。
运算符赋值变体的子例程只需要返回操作的结果。它可以更改其操作数的值(这是安全的,因为 Perl 首先调用复制构造函数),但这可选项,因为 Perl 无论如何都会将返回值分配给左操作数。
重载赋值运算符的对象仅在对该对象的赋值方面这样做。换句话说,Perl 永远不会调用具有第三个参数(“交换”参数)设置为 TRUE 的相应方法。例如,操作
$a *= $b
即使 $a
是一个标量,也不会导致调用 $b
的 *=
实现。 (但是,它可以生成对 $b
的 *
方法的调用)。
非变异方法与变异方法变体
+ - * / % ** << >> x .
& | ^ &. |. ^.
如 上面 所述,Perl 可能会在实现 ++
、+=
和 &=
等缺失操作的过程中调用 +
和 &
等运算符的方法。 虽然这些方法可以通过测试第三个参数的定义来检测这种用法,但它们在所有情况下都应该避免更改其操作数。 这是因为 Perl 在调用这些方法之前不会调用复制构造函数。
整数
传统上,Perl 函数 int
向 0 四舍五入(参见 "int" in perlfunc),因此对于类似浮点数的类型,应该遵循相同的语义。
字符串、数字、布尔值和正则表达式转换
"" 0+ bool
这些转换根据需要在上下文中调用。 例如,'""'
(字符串化)的子例程可能在将重载对象作为参数传递给 print
时使用,而 'bool'
的子例程可能在它被测试为流控制语句(如 while
)或三元 ?:
操作的条件时使用。
当然,在像 $obj + 1
这样的上下文中,Perl 将调用 $obj
的 +
实现,而不是(在本例中)使用 numify 方法 '0+'
将 $obj
转换为数字(一个例外是当没有为 '+'
提供方法并且 "fallback" 设置为 TRUE 时)。
'""'
、'0+'
和 'bool'
的子例程可以返回任何任意的 Perl 值。 如果此值的相应操作也被重载,则将使用此值再次调用该操作。
作为特殊情况,如果重载返回对象本身,则将直接使用它。 重载转换返回对象可能是一个错误,因为你可能会得到类似 YourPackage=HASH(0x8172b34)
的东西。
qr
'qr'
的子例程在将对象插入或用作正则表达式时使用,包括当它出现在 =~
或 !~
运算符的 RHS 时。
qr
必须返回一个已编译的正则表达式,或对已编译正则表达式的引用(例如 qr//
返回),并且对返回值的任何进一步重载将被忽略。
迭代
如果<>
被重载,那么相同的实现将用于读取文件句柄语法<$var>
和通配符语法<${var}>
。
文件测试
键'-X'
用于指定一个子例程来处理所有文件测试运算符(-f
、-x
等:有关完整列表,请参见perlfunc 中的 "-X");无法单独重载任何文件测试运算符。为了区分它们,在 '-' 之后的字母将作为第二个参数传递(也就是说,在二元运算符用来传递第二个操作数的槽中)。
调用重载的文件测试运算符不会影响与特殊文件句柄_
关联的 stat 值。它仍然引用最后一次stat
、lstat
或未重载的文件测试的结果。
此重载是在 Perl 5.12 中引入的。
匹配
键"~~"
允许您覆盖~~
运算符和 switch 结构(given
/when
)使用的智能匹配逻辑。请参见perlsyn 中的 "Switch Statements" 和feature。
不寻常的是,智能匹配运算符的重载实现并没有完全控制智能匹配的行为。特别是在以下代码中
package Foo;
use overload '~~' => 'match';
my $obj = Foo->new();
$obj ~~ [ 1,2,3 ];
智能匹配不会像这样调用方法
$obj->match([1,2,3],0);
相反,智能匹配分配规则优先,因此 $obj 依次与数组中的每个元素进行智能匹配,直到找到匹配项,因此您可能会看到一到三个这样的调用
$obj->match(1,0);
$obj->match(2,0);
$obj->match(3,0);
有关何时调用重载的详细信息,请参阅perlop 中的 "Smartmatch Operator" 中的匹配表。
解引用
${} @{} %{} &{} *{}
如果这些运算符没有被显式重载,那么它们将以正常方式工作,生成底层的标量、数组或存储对象数据的任何内容(如果解引用运算符与之不匹配,则生成相应的错误消息)。定义一个通用的'nomethod'
(参见下面)不会对此产生任何影响,因为不会调用通用函数来实现缺少的解引用运算符。
如果解引用运算符被重载,那么它必须返回一个引用,该引用具有适当的类型(例如,键'${}'
的子例程应该返回一个指向标量的引用,而不是一个标量),或者另一个重载运算符的对象:也就是说,子例程只确定要解引用的内容,而实际的解引用留给 Perl 处理。作为一种特殊情况,如果子例程返回对象本身,那么它将不会再次被调用 - 避免无限递归。
特殊
nomethod fallback =
如果找不到操作的方法,Perl 会尝试从已定义的操作中自动生成替代实现。
注意:本节中描述的行为可以通过将 fallback
设置为 FALSE 来禁用(参见 "fallback")。
在下表中,数字表示优先级。例如,下表说明,如果未定义 '!'
的实现,则 Perl 将使用 'bool'
来实现它(即,通过反转 'bool'
方法返回的值);如果布尔转换也未实现,则 Perl 将使用 '0+'
,或者如果失败,则使用 '""'
。
operator | can be autogenerated from
|
| 0+ "" bool . x
=========|==========================
0+ | 1 2
"" | 1 2
bool | 1 2
int | 1 2 3
! | 2 3 1
qr | 2 1 3
. | 2 1 3
x | 2 1 3
.= | 3 2 4 1
x= | 3 2 4 1
<> | 2 1 3
-X | 2 1 3
注意:迭代器 ('<>'
) 和文件测试 ('-X'
) 运算符按正常方式工作:如果操作数不是已祝福的 glob 或 IO 引用,则将其转换为字符串(使用 '""'
、'0+'
或 'bool'
的方法)以解释为 glob 或文件名。
operator | can be autogenerated from
|
| < <=> neg -= -
=========|==========================
neg | 1
-= | 1
-- | 1 2
abs | a1 a2 b1 b2 [*]
< | 1
<= | 1
> | 1
>= | 1
== | 1
!= | 1
* one from [a1, a2] and one from [b1, b2]
正如数值比较可以从 '<=>'
的方法自动生成一样,字符串比较可以从 'cmp'
的方法自动生成。
operators | can be autogenerated from
====================|===========================
lt gt le ge eq ne | cmp
类似地,'+='
和 '++'
键的自动生成类似于上面的 '-='
和 '--'
operator | can be autogenerated from
|
| += +
=========|==========================
+= | 1
++ | 1 2
其他赋值变体类似于 '+='
和 '-='
(类似于上面的 '.='
和 'x='
)
operator || *= /= %= **= <<= >>= &= ^= |= &.= ^.= |.=
-------------------||-------------------------------------------
autogenerated from || * / % ** << >> & ^ | &. ^. |.
还要注意,复制构造函数(键 '='
)可能会自动生成,但仅适用于基于标量的对象。参见 "复制构造函数"。
由于某些操作可以从其他操作自动生成,因此需要重载一组最小的操作,以便可以使用完整的重载操作集。当然,自动生成的运算符可能无法完全按照用户的预期执行。最小集是
+ - * / % ** << >> x
<=> cmp
& | ^ ~ &. |. ^. ~.
atan2 cos sin exp log sqrt int
"" 0+ bool
~~
在转换中,只需要字符串、布尔值或数值中的一个,因为每个都可以从另外两个生成。
use overload
的特殊键nomethod
'nomethod'
键用于指定一个通用的函数,用于处理任何未单独重载的操作符。指定的函数将传递四个参数。前三个参数与如果定义了相应方法将传递给该方法的参数一致。第四个参数是该缺失方法的 use overload
键。如果启用了“按位”功能(参见 feature),则将向处理 &
、|
、^
和 ~
的子例程传递第五个 TRUE 参数,以指示调用者期望数值行为。
例如,如果 $a
是一个被祝福到声明以下内容的包中的对象
use overload 'nomethod' => 'catch_all', # ...
那么操作
3 + $a
可能(除非为 '+'
键专门声明了一个方法)导致调用
catch_all($a, 3, 1, '+')
参见 "Perl 如何选择操作符实现"。
fallback
分配给 'fallback'
键的值告诉 Perl 它应该尝试多努力才能找到实现缺失操作符的替代方法。
已定义,但为 FALSE
use overload "fallback" => 0, # ... ;
这将禁用 "神奇自动生成"。
undef
在默认情况下,如果未明确为 fallback
分配值,则启用神奇自动生成。
TRUE
与 undef
相同,但如果无法自动生成缺失的操作符,则 Perl 被允许恢复到如果没有 use overload
指令,它将对该操作符执行的操作,而不是发出错误消息。
注意:在大多数情况下,特别是 "复制构造函数",这不太可能是适当的行为。
参见 "Perl 如何选择操作符实现"。
如 上面 所述,当对与其他引用共享其对象的引用应用变异器时,将调用此操作。例如,如果 $b
是数学魔法的,'++'
被重载为 'incr'
,'='
被重载为 'clone'
,那么代码
$a = $b;
# ... (other code which does not modify $a or $b) ...
++$b;
将以等效于以下方式执行
$a = $b;
# ...
$b = $b->clone(undef, "");
$b->incr(undef, "");
注意
'='
的子例程不会重载 Perl 赋值运算符:它仅用于允许变异器按此处所述工作。(参见上面的 "赋值"。)
对于其他操作,实现 '='
的子例程传递三个参数,尽管最后两个始终为 undef
和 ''
。
复制构造函数仅在调用声明为实现变异器的函数之前调用,例如,如果上面的代码中的 ++$b;
通过为 '++'
键(或 'nomethod'
,传递 '++'
作为第四个参数)声明的方法或通过自动生成 '+='
来实现,则会调用它。如果递增操作通过调用 '+'
的方法来实现,则不会调用它,因为在等效代码中,
$a = $b;
$b = $b + 1;
$a
所引用的数据在将对新对象数据的引用赋值给 $b
时不会改变。
如果 Perl 确定不需要复制构造函数(因为没有其他引用指向正在修改的数据),则不会调用复制构造函数。
如果 'fallback'
未定义或为 TRUE,则可以自动生成复制构造函数,但仅适用于基于标量的对象。在其他情况下,需要显式定义它。如果对象的存储数据例如是标量数组,则以下方法可能适用
use overload '=' => sub { bless [ @{$_[0]} ] }, # ...
如果 'fallback'
为 TRUE 且未定义复制构造函数,则对于不基于标量的对象,Perl 可能会静默地回退到简单赋值 - 也就是说,赋值对象引用。实际上,这会禁用复制构造函数机制,因为不会创建对象数据的任何新副本。这几乎肯定不是你想要的。(然而,它是一致的:例如,Perl 对 ++
运算符的回退是递增引用本身。)
首先检查 nomethod
还是 fallback
?如果运算符的两个操作数类型不同,并且都重载了运算符,则使用哪个实现?以下是优先级规则
如果第一个操作数声明了一个子例程来重载运算符,则使用该实现。
否则,如果第一个操作数的 fallback
为 TRUE 或未定义,则查看 自动生成规则 是否允许使用其其他运算符。
除非运算符是赋值(+=
、-=
等),否则重复步骤 (1) 以考虑第二个操作数。
重复步骤 (2) 以考虑第二个操作数。
如果第一个操作数具有 "nomethod" 方法,则使用该方法。
如果第二个操作数具有 "nomethod" 方法,则使用该方法。
如果两个操作数的 fallback
都为 TRUE,则对运算符执行通常的操作,将操作数视为数字、字符串或布尔值,具体取决于运算符(参见注释)。
什么都没用 - 退出。
如果只有一个操作数(或只有一个操作数具有重载),则跳过上述关于另一个操作数的检查。
对于解引用操作,上述规则存在例外(如果步骤 1 失败,则始终回退到正常的内置实现 - 请参阅解引用),以及对于 ~~
(它有自己的规则集 - 请参阅 "可重载操作" 上面的“匹配”)。
关于步骤 7 的说明:一些运算符根据其操作数的类型具有不同的语义。由于没有办法指示 Perl 将操作数视为例如数字而不是字符串,因此这里的结果可能不是您预期的。请参阅 "错误和陷阱"。
比较操作的限制是,即使例如 cmp
应该返回一个祝福的引用,自动生成的 lt
函数也将仅基于 cmp
结果的数值产生一个标准的逻辑值。特别是在这种情况下,需要一个有效的数值转换(可能用其他转换表示)。
类似地,.=
和 x=
运算符如果应用了字符串转换替换,则会失去其数学特性。
当您对一个数学对象进行 chop()
操作时,它会被提升为字符串,并且其数学特性会丢失。其他操作也可能发生这种情况。
重载通过 @ISA 层次结构尊重继承。继承以两种方式与重载交互。
use overload
指令中的方法名称如果
use overload key => value;
中的 value
是一个字符串,它将被解释为一个方法名称 - 该名称可能(以通常的方式)从另一个类继承。
从重载类派生的任何类也被重载并继承其运算符实现。如果在多个祖先中重载了相同的运算符,则实现由通常的继承规则确定。
例如,如果 A
从 B
和 C
(按此顺序)继承,B
用 \&D::plus_sub
重载 +
,而 C
用 "plus_meth"
重载 +
,那么子例程 D::plus_sub
将被调用来实现包 A
中对象的运算 +
。
请注意,在 Perl 5.18 之前的版本中,fallback
键的继承不受上述规则的约束。第一个重载祖先中的 fallback
值被使用。这在 5.18 中得到修复,以遵循通常的继承规则。
由于所有 use
指令都在编译时执行,因此在运行时更改重载的唯一方法是
eval 'use overload "+" => \&addmethod';
您也可以使用
eval 'no overload "+", "--", "<="';
尽管在运行时使用这些结构值得怀疑。
包 overload.pm
提供以下公共函数
返回 arg
的字符串值,如同没有字符串化重载一样。如果您使用此函数来获取引用的地址(用于检查两个引用是否指向同一对象),那么您最好使用 builtin::refaddr()
或 Scalar::Util::refaddr()
,它们更快。
如果 arg
受到某些操作的重载,则返回 true。
返回 undef
或实现 op
的方法的引用。
这样的方法总是接受三个参数,如果它是 XS 方法,则会强制执行。
对于某些应用程序,Perl 解析器对常量的处理过于复杂。可以通过 overload::constant()
和 overload::remove_constant()
函数来介入此过程。
这些函数接受一个哈希作为参数。此哈希的识别键是
用于重载整数常量,
用于重载浮点数常量,
用于重载八进制和十六进制常量,
用于重载 q
引用的字符串、qq
和 qx
引用的字符串的常量部分以及 here-document,
用于重载正则表达式的常量部分。
相应的数值是函数的引用,这些函数接受三个参数:第一个是常量的初始字符串形式,第二个是 Perl 如何解释该常量,第三个是常量的使用方式。请注意,初始字符串形式不包含字符串分隔符,并且在反斜杠分隔符组合中去除了反斜杠(因此分隔符的值与该字符串的处理无关)。该函数的返回值是 Perl 如何解释该常量。第三个参数除非是重载的q
- 和 qr
- 常量,否则未定义,在单引号上下文中为 q
(来自字符串、正则表达式和单引号 HERE 文档),对于 tr
/y
运算符的参数为 tr
,对于 s
运算符的右侧为 s
,否则为 qq
。
由于表达式 "ab$cd,,"
只是 'ab' . $cd . ',,'
的快捷方式,因此期望重载的常量字符串配备合理的重载连接运算符,否则会导致荒谬的结果。类似地,负数被视为正常量的否定。
请注意,从除 import() 和 unimport() 方法之外的任何地方调用函数 overload::constant() 和 overload::remove_constant() 可能毫无意义。从这些方法中,它们可以被调用为
sub import {
shift;
return unless @_;
die "unknown import: @_" unless @_ == 1 and $_[0] eq ':constant';
overload::constant integer => sub {Math::BigInt->new(shift)};
}
以下内容可能会随时更改。
所有操作的方法表缓存在包的符号表哈希的魔术中。在处理 use overload
、no overload
、新的函数定义以及 @ISA 中的更改时,缓存将失效。
(每个 SVish 东西都有一个魔术队列,魔术是该队列中的一个条目。这就是单个变量可以同时参与多种魔术形式的方式。例如,环境变量通常同时具有两种形式:它们的 %ENV 魔术和它们的污染魔术。但是,实现重载的魔术应用于存储区,存储区很少直接使用,因此不应该减慢 Perl 的速度。)
如果一个包使用重载,它会携带一个特殊的标志。当定义新函数或修改 @ISA 时,也会设置此标志。在支持重载的第一个操作之后,会有一点速度损失,因为此时重载表正在更新。如果不存在重载,则会关闭此标志。因此,此后唯一的速度损失是检查此标志。
预期对不应显式更改的方法的参数是常量(但不会强制执行)。
请在以下内容中添加示例!
将此代码放在 Perl 库目录中的 two_face.pm 文件中
package two_face; # Scalars with separate string and
# numeric values.
sub new { my $p = shift; bless [@_], $p }
use overload '""' => \&str, '0+' => \&num, fallback => 1;
sub num {shift->[1]}
sub str {shift->[0]}
使用方法如下
require two_face;
my $seven = two_face->new("vii", 7);
printf "seven=$seven, seven=%d, eight=%d\n", $seven, $seven+1;
print "seven contains 'i'\n" if $seven =~ /i/;
(第二行创建了一个标量,它同时具有字符串值和数值。)这将打印
seven=vii, seven=7, eight=8
seven contains 'i'
假设您想创建一个既可以作为数组引用,也可以作为哈希引用的对象。
package two_refs;
use overload '%{}' => \&gethash, '@{}' => sub { $ {shift()} };
sub new {
my $p = shift;
bless \ [@_], $p;
}
sub gethash {
my %h;
my $self = shift;
tie %h, ref $self, $self;
\%h;
}
sub TIEHASH { my $p = shift; bless \ shift, $p }
my %fields;
my $i = 0;
$fields{$_} = $i++ foreach qw{zero one two three};
sub STORE {
my $self = ${shift()};
my $key = $fields{shift()};
defined $key or die "Out of band access";
$$self->[$key] = shift;
}
sub FETCH {
my $self = ${shift()};
my $key = $fields{shift()};
defined $key or die "Out of band access";
$$self->[$key];
}
现在可以使用数组和哈希语法访问对象
my $bar = two_refs->new(3,4,5,6);
$bar->[2] = 11;
$bar->{two} == 11 or die 'bad hash fetch';
请注意此示例的几个重要特征。首先,$bar 的实际类型是标量引用,我们没有重载标量解引用。因此,我们可以通过使用 $$bar
(我们在重载解引用函数中所做的事情)来获取 $bar 的实际非重载内容。类似地,TIEHASH() 方法返回的对象是标量引用。
其次,每次使用哈希语法时,我们都会创建一个新的绑定哈希。这使我们不必担心出现引用循环的可能性,这会导致内存泄漏。
这两个问题都可以解决。例如,如果我们想在对实现为哈希本身的对象的引用上重载哈希解引用,唯一需要解决的问题是如何访问这个实际哈希(与虚拟哈希不同,虚拟哈希由重载的解引用运算符展示)。以下是一个可能的获取例程
sub access_hash {
my ($self, $key) = (shift, shift);
my $class = ref $self;
bless $self, 'overload::dummy'; # Disable overloading of %{}
my $out = $self->{$key};
bless $self, $class; # Restore overloading
$out;
}
为了避免在每次访问时创建绑定哈希,可以使用额外的间接级别,这允许非循环的引用结构
package two_refs1;
use overload
'%{}' => sub { ${shift()}->[1] },
'@{}' => sub { ${shift()}->[0] };
sub new {
my $p = shift;
my $a = [@_];
my %h;
tie %h, $p, $a;
bless \ [$a, \%h], $p;
}
sub gethash {
my %h;
my $self = shift;
tie %h, ref $self, $self;
\%h;
}
sub TIEHASH { my $p = shift; bless \ shift, $p }
my %fields;
my $i = 0;
$fields{$_} = $i++ foreach qw{zero one two three};
sub STORE {
my $a = ${shift()};
my $key = $fields{shift()};
defined $key or die "Out of band access";
$a->[$key] = shift;
}
sub FETCH {
my $a = ${shift()};
my $key = $fields{shift()};
defined $key or die "Out of band access";
$a->[$key];
}
现在如果 $baz 被重载成这样,那么 $baz
就是对中间数组的引用的引用,该数组保存对实际数组和访问哈希的引用。访问哈希的 tie() 对象是对实际数组的引用的引用,所以
没有引用循环。
两个被祝福到 two_refs1
类中的“对象”都是对数组的引用的引用,因此是对标量的引用。因此访问器表达式 $$foo->[$ind]
不涉及任何重载操作。
将此代码放入 Perl 库目录中的 symbolic.pm 文件中
package symbolic; # Primitive symbolic calculator
use overload nomethod => \&wrap;
sub new { shift; bless ['n', @_] }
sub wrap {
my ($obj, $other, $inv, $meth) = @_;
($obj, $other) = ($other, $obj) if $inv;
bless [$meth, $obj, $other];
}
这个模块非常不寻常,因为它没有提供任何常见的重载运算符,而是为 "nomethod"
提供了实现。在这个例子中,nomethod
子例程返回一个对象,该对象封装了对对象执行的操作:symbolic->new(3)
包含 ['n', 3]
,2 + symbolic->new(3)
包含 ['+', 2, ['n', 3]]
。
以下是一个使用上述包“计算”外接八边形边长的脚本示例
require symbolic;
my $iter = 1; # 2**($iter+2) = 8
my $side = symbolic->new(1);
my $cnt = $iter;
while ($cnt--) {
$side = (sqrt(1 + $side**2) - 1)/$side;
}
print "OK\n";
$side 的值为
['/', ['-', ['sqrt', ['+', 1, ['**', ['n', 1], 2]],
undef], 1], ['n', 1]]
请注意,虽然我们使用了一个简单的脚本获得了这个值,但没有简单的方法可以使用这个值。事实上,这个值可以在调试器中检查(参见 perldebug),但前提是设置了 bareStringify
Option,而不是通过 p
命令。
如果尝试打印这个值,那么重载运算符 ""
将被调用,这将调用 nomethod
运算符。这个运算符的结果将再次被字符串化,但这个结果又是 symbolic
类型,这将导致无限循环。
在 symbolic.pm 模块中添加一个漂亮打印方法
sub pretty {
my ($meth, $a, $b) = @{+shift};
$a = 'u' unless defined $a;
$b = 'u' unless defined $b;
$a = $a->pretty if ref $a;
$b = $b->pretty if ref $b;
"[$meth $a $b]";
}
现在可以使用以下代码完成脚本
print "side = ", $side->pretty, "\n";
pretty
方法执行对象到字符串的转换,因此使用此方法重载运算符 ""
是自然的。但是,在这样的方法内部,没有必要对对象的组件 $a 和 $b 进行漂亮打印。在上面的子例程中,"[$meth $a $b]"
是某些字符串和组件 $a 和 $b 的连接。如果这些组件使用重载,连接运算符将查找重载运算符 .
;如果不存在,它将查找重载运算符 ""
。因此,使用以下代码就足够了
use overload nomethod => \&wrap, '""' => \&str;
sub str {
my ($meth, $a, $b) = @{+shift};
$a = 'u' unless defined $a;
$b = 'u' unless defined $b;
"[$meth $a $b]";
}
现在可以将脚本的最后一行更改为
print "side = $side\n";
这将输出
side = [/ [- [sqrt [+ 1 [** [n 1 u] 2]] u] 1] [n 1 u]]
并且可以使用所有可能的方法在调试器中检查该值。
仍然存在一些问题:考虑脚本的循环变量 $cnt。它是一个数字,而不是一个对象。我们不能将这个值设置为 symbolic
类型,因为这样循环将不会终止。
确实,要终止循环,$cnt 应该变为假。但是,用于检查假值的运算符 bool
被重载(这次通过重载的 ""
),并返回一个长字符串,因此任何类型为 symbolic
的对象都是真值。为了克服这个问题,我们需要一种方法来比较对象与 0。事实上,编写一个数值转换例程更容易。
以下是添加了此类例程(并略微修改了 str())的 symbolic.pm 文本。
package symbolic; # Primitive symbolic calculator
use overload
nomethod => \&wrap, '""' => \&str, '0+' => \#
sub new { shift; bless ['n', @_] }
sub wrap {
my ($obj, $other, $inv, $meth) = @_;
($obj, $other) = ($other, $obj) if $inv;
bless [$meth, $obj, $other];
}
sub str {
my ($meth, $a, $b) = @{+shift};
$a = 'u' unless defined $a;
if (defined $b) {
"[$meth $a $b]";
} else {
"[$meth $a]";
}
}
my %subr = (
n => sub {$_[0]},
sqrt => sub {sqrt $_[0]},
'-' => sub {shift() - shift()},
'+' => sub {shift() + shift()},
'/' => sub {shift() / shift()},
'*' => sub {shift() * shift()},
'**' => sub {shift() ** shift()},
);
sub num {
my ($meth, $a, $b) = @{+shift};
my $subr = $subr{$meth}
or die "Do not know how to ($meth) in symbolic";
$a = $a->num if ref $a eq __PACKAGE__;
$b = $b->num if ref $b eq __PACKAGE__;
$subr->($a,$b);
}
所有数值转换的工作都在 %subr 和 num() 中完成。当然,%subr 并不完整,它只包含下面示例中使用的运算符。这是一个额外的积分问题:为什么我们需要在 num() 中显式递归?(答案在本节末尾。)
像这样使用此模块
require symbolic;
my $iter = symbolic->new(2); # 16-gon
my $side = symbolic->new(1);
my $cnt = $iter;
while ($cnt) {
$cnt = $cnt - 1; # Mutator '--' not implemented
$side = (sqrt(1 + $side**2) - 1)/$side;
}
printf "%s=%f\n", $side, $side;
printf "pi=%f\n", $side*(2**($iter+2));
它打印(没有那么多换行符)
[/ [- [sqrt [+ 1 [** [/ [- [sqrt [+ 1 [** [n 1] 2]]] 1]
[n 1]] 2]]] 1]
[/ [- [sqrt [+ 1 [** [n 1] 2]]] 1] [n 1]]]=0.198912
pi=3.182598
上面的模块非常原始。它没有实现 mutator 方法(++
、-=
等),没有进行深层复制(没有 mutator 不需要!),并且只实现了示例中使用的算术运算。
实现大多数算术运算很容易;只需使用运算表,并将填充 %subr 的代码更改为
my %subr = ( 'n' => sub {$_[0]} );
foreach my $op (split " ", $overload::ops{with_assign}) {
$subr{$op} = $subr{"$op="} = eval "sub {shift() $op shift()}";
}
my @bins = qw(binary 3way_comparison num_comparison str_comparison);
foreach my $op (split " ", "@overload::ops{ @bins }") {
$subr{$op} = eval "sub {shift() $op shift()}";
}
foreach my $op (split " ", "@overload::ops{qw(unary func)}") {
print "defining '$op'\n";
$subr{$op} = eval "sub {$op shift()}";
}
由于实现赋值运算符的子例程不需要修改其操作数(见上面 "可重载运算"),因此除了将这些运算符添加到 %subr 并定义一个复制构造函数(因为 Perl 没有办法知道 '+='
的实现不会改变参数 - 见 "复制构造函数")之外,我们不需要任何特殊的东西来使 +=
及其朋友工作。
要实现复制构造函数,请将 '=' => \&cpy
添加到 use overload
行,以及代码(此代码假设 mutator 只改变一层深度的内容,因此不需要递归复制)
sub cpy {
my $self = shift;
bless [@$self], ref $self;
}
要使 ++
和 --
工作,我们需要实现实际的 mutator,无论是直接实现,还是在 nomethod
中实现。我们继续在 nomethod
中做事情,因此添加
if ($meth eq '++' or $meth eq '--') {
@$obj = ($meth, (bless [@$obj]), 1); # Avoid circular reference
return $obj;
}
在 wrap() 的第一行之后。这不是最有效的实现,可以考虑
sub inc { $_[0] = bless ['++', shift, 1]; }
代替。
最后,请注意,可以通过以下方式填充 %subr:
my %subr = ( 'n' => sub {$_[0]} );
foreach my $op (split " ", $overload::ops{with_assign}) {
$subr{$op} = $subr{"$op="} = eval "sub {shift() $op shift()}";
}
my @bins = qw(binary 3way_comparison num_comparison str_comparison);
foreach my $op (split " ", "@overload::ops{ @bins }") {
$subr{$op} = eval "sub {shift() $op shift()}";
}
foreach my $op (split " ", "@overload::ops{qw(unary func)}") {
$subr{$op} = eval "sub {$op shift()}";
}
$subr{'++'} = $subr{'+'};
$subr{'--'} = $subr{'-'};
这段代码用 50 行 Perl 代码实现了简单的符号计算器。由于子表达式的数值没有被缓存,所以计算器速度很慢。
以下是练习的答案:在 str() 的情况下,我们不需要显式递归,因为重载的 .
运算符将回退到现有的重载运算符 ""
。如果未显式请求 fallback
,则重载的算术运算符 *不会* 回退到数值转换。因此,如果没有显式递归,num() 将把 ['+', $a, $b]
转换为 $a + $b
,这将只是重新构建 num() 的参数。
如果您想知道为什么 str() 和 num() 的转换默认值不同,请注意编写符号计算器是多么容易。这种简单性是由于对默认值的适当选择。另外一点:由于显式递归,num() 比 sym() 更脆弱:我们需要显式检查 $a 和 $b 的类型。如果组件 $a 和 $b 碰巧是某种相关类型,这可能会导致问题。
人们可能会想知道为什么我们将上面的计算器称为符号计算器。原因是表达式值的实际计算被推迟到该值被 *使用* 时。
要查看它的实际效果,请向 symbolic
包添加一个方法
sub STORE {
my $obj = shift;
$#$obj = 1;
@$obj->[0,1] = ('=', shift);
}
更改后,您可以执行
my $a = symbolic->new(3);
my $b = symbolic->new(4);
my $c = sqrt($a**2 + $b**2);
此时 $c 的数值变为 5。但是,在调用
$a->STORE(12); $b->STORE(5);
之后,$c 的数值变为 13。现在毫无疑问,symbolic 模块确实提供了一个 *符号* 计算器。
为了隐藏底层代码的粗糙边缘,请为 symbolic
包提供一个 tie()d 接口。添加方法
sub TIESCALAR { my $pack = shift; $pack->new(@_) }
sub FETCH { shift }
sub nop { } # Around a bug
(Perl 5.14 中修复了这个错误,在 "BUGS" 中有描述)。您可以使用这个新接口,例如
tie $a, 'symbolic', 3;
tie $b, 'symbolic', 4;
$a->nop; $b->nop; # Around a bug
my $c = sqrt($a**2 + $b**2);
现在 $c 的数值为 5。在 $a = 12; $b = 5
之后,$c 的数值变为 13。为了隔离模块的用户,请添加一个方法
sub vars { my $p = shift; tie($_, $p), $_->nop foreach @_; }
现在
my ($a, $b);
symbolic->vars($a, $b);
my $c = sqrt($a**2 + $b**2);
$a = 3; $b = 4;
printf "c5 %s=%f\n", $c, $c;
$a = 12; $b = 5;
printf "c13 %s=%f\n", $c, $c;
显示 $c 的数值会随着 $a 和 $b 值的变化而变化。
Ilya Zakharevich <[email protected]>。
overloading
编译指示可以用来在词法范围内启用或禁用重载操作 - 请参阅 overloading。
当 Perl 使用 -Do 开关或其等效项运行时,重载会产生诊断信息。
使用 Perl 调试器 (参见 perldebug) 的 m
命令,可以推断出哪些操作被重载了(以及哪个祖先触发了这种重载)。例如,如果 eq
被重载,那么调试器会显示方法 (eq
。方法 ()
对应于 fallback
键(实际上,该方法的存在表明该包启用了重载,并且它是模块 overload
的 Overloaded
函数所使用的)。
该模块可能会发出以下警告
(W) 对 overload::constant 的调用包含奇数个参数。参数应该成对出现。
(W) 您尝试重载 overload 包不知道的常量类型。
(W) overload::constant 的第二个(第四个,第六个,...)参数需要是一个代码引用。可以是匿名子程序,也可以是子程序的引用。
(W) use overload
传递了一个它不认识的参数。您是否拼错了运算符?
当 fallback 为 TRUE 并且 Perl 回退到运算符的内置实现时,一个陷阱是某些运算符具有多个语义,例如 |
use overload '0+' => sub { $_[0]->{n}; },
fallback => 1;
my $x = bless { n => 4 }, "main";
my $y = bless { n => 8 }, "main";
print $x | $y, "\n";
您可能期望这输出 "12"。实际上,它打印 "<":将 "|" 视为按位字符串运算符的结果 - 也就是说,将操作数视为字符串 "4" 和 "8" 而不是数字的结果。numify (0+
) 已实现但 stringify (""
) 未实现的事实无关紧要,因为后者只是从前者自动生成的。
更改此行为的唯一方法是为 '|'
提供您自己的子程序。
神奇的自动生成增加了意外创建自引用结构的可能性。目前,Perl 不会释放自引用结构,直到显式地打破循环。例如,
use overload '+' => 'add';
sub add { bless [ \$_[0], \$_[1] ] };
正在寻求麻烦,因为
$obj += $y;
将有效地变为
$obj = add($obj, $y, undef);
与
$obj = [\$obj, \$foo];
的结果相同。即使脚本中没有显式的运算符赋值变体,它们也可能由优化器生成。例如,
"obj = $obj\n"
可以优化为
my $tmp = 'obj = ' . $obj; $tmp .= "\n";
符号表充满了看起来像线路噪音的名称。
此错误已在 Perl 5.18 中修复,但如果您使用的是旧版本,它仍然可能让您措手不及。
为了继承的目的,每个重载包的行为都好像存在 `fallback`(可能未定义)。如果某个包未重载,但继承自两个重载包,这可能会产生有趣的效果。
在 Perl 5.14 之前,重载和 tie() 之间的关系被破坏了。重载是否被触发取决于 tie() 变量的先前类。
发生这种情况是因为在尝试任何 tie() 访问之前,过早地检查了重载的存在。如果从绑定的变量中 FETCH() 的值的类没有改变,那么对于要在旧版 Perl 版本上运行的代码,一个简单的解决方法是在 tie() 之后立即访问该值(通过 `() = $foo` 或类似方法),这样在这次调用之后,先前的类就与当前类一致了。
裸词不受重载字符串常量的覆盖。
范围运算符 `..` 不能重载。