需要帮助理解 Perl 5 如何解析引用相同变量的复合赋值语句

Need help understanding how Perl 5 is parsing a compound assignment statement referencing same variable

我需要了解如何计算这个简单的表达式,因为结果与我预期的不同。

我对 Perl 还很陌生,但我认为我有足够的理解来解释这个看似简单的片段的结果。显然我错过了一些东西。 我已经使用 Deparse 查看 Perl 如何处理表达式,Deparse 不会更改我已经放置的括号。

$i = 12;
$i = (($i /= 2) + ($i = 100));
print $i;

根据我的理解,结果应该是 106,假设表达式是按照括号中指示的顺序计算的,并且看起来应该是这样。我会想: $i 首先除以 2,从而将 6 赋给 $i,结果值为 6。然后将 100 赋给 $i,100 是第二个表达式的结果。 6 + 100 = 106,我认为最终会分配给 $i。 相反,它打印“200”。

在 PHP 中,相同的代码确实产生了“106”,这使我相信这与表达式的某些部分被解释为列表有关,或者与 Perl 一样精彩。 迫不及待地想知道我做错了什么。

到目前为止,大多数语言都没有定义当您在单个表达式中读写相同的值变量时会发生什么。 Perl 也不例外。您发布的表达式没有定义的结果。

这在 perlop 中有记录:

modifying a variable twice in the same statement will lead to undefined behavior. Avoid statements like:

    $i = $i ++;
    print ++ $i + $i ++;

发生的事情是 $i /= 2$i = 100 都 return $i — 不是 $i 的值,而是 $i 本身 —所以你最终做 $i + $i 而不是 6 + 100。你不能指望这种行为。此外,Perl 碰巧先计算加法的左操作数,然后再计算它的右操作数——这是你不能指望的事​​情——所以 $i100 是时候执行加法了。

如果有人想开玩笑,这里是 当前perl 评估 OP 代码时发生的事情的再现:

use strict;
use warnings;
use feature qw( say );
use experimental qw( refaliasing declared_refs );

my $i = 12;

my @ST;  # Stack
{                                      $ST[@ST] = \( $i                    ); }
{                                      $ST[@ST] = \( 2                     ); }
{ \my ($lhs, $rhs) = \splice(@ST, -2); $ST[@ST] = \( $lhs /= $rhs          ); }
{                                      $ST[@ST] = \( 100                   ); }
{                                      $ST[@ST] = \( $i                    ); }
{ \my ($rhs, $lhs) = \splice(@ST, -2); $ST[@ST] = \( $lhs = $rhs           ); }
{ \my ($lhs, $rhs) = \splice(@ST, -2); $ST[@ST] = \( my $sum = $lhs + $rhs ); }
{                                      $ST[@ST] = \( $i                    ); }
{ \my ($rhs, $lhs) = \splice(@ST, -2); $ST[@ST] = \( $lhs = $rhs           ); }

say $i;  # 200

如果你使用Devel::Peek的Dump,你会注意到上面的大部分变量都有相同的地址。在 Perl 术语中,它们就是我们所说的 "aliases"。

以下使用引用代替(尽管实际上没有创建实际引用):

use strict;
use warnings;
use feature qw( say );

my $i = 12;

my @ST;  # Stack
{                                         push @ST, \( $i                          ); }
{                                         push @ST, \( 2                           ); }
{ my ($lhs_p, $rhs_p) = splice(@ST, -2);  push @ST, \( $$lhs_p /= $$rhs_p          ); }
{                                         push @ST, \( 100                         ); }
{                                         push @ST, \( $i                          ); }
{ my ($rhs_p, $lhs_p) = splice(@ST, -2);  push @ST, \( $$lhs_p = $$rhs_p           ); }
{ my ($lhs_p, $rhs_p) = splice(@ST, -2);  push @ST, \( my $sum = $$lhs_p + $$rhs_p ); }
{                                         push @ST, \( $i                          ); }
{ my ($rhs_p, $lhs_p) = splice(@ST, -2);  push @ST, \( $$lhs_p = $$rhs_p           ); }

say $i;  # 200

perl 的美妙之处在于参数(无论是左值还是右值)总是作为对实际变量的引用而不是它们值的副本传递给 perl 运算符。这与大多数其他语言不同,并且符合 perl 是一种传递引用的语言(如 Fortran)这一事实。

你的例子是一个非常不幸的 red-herring,因为它假定 + 的操作数是从左到右计算的,这(虽然绝对正确对于 只有 可用的 perl5 实现)是 afaik,任何文档都不保证。

让我们用 comma 运算符试试,确实[1] 保证从左到右计算其参数:

perl -le 'print @y = ($x = 1, $x = 2, $x = 3)'

应该打印 123 对吧?

不,因为 perl 会首先从左到右计算所有赋值,每个赋值返回 $x不是它的一个副本,并且 then 将通过取消引用它进行 3 次 "resolve" 它,每次都获取存储在其中的最后一个值。于是 333.

[1]: 来自 perlop(1): "Comma operator ... In list context, it's just the list argument separator, and inserts both its arguments into the list. These arguments are also evaluated from left to right".

[上面的额外赋值是为了避免讨论 perl 中参数列表如何以及为什么实际上是一个列表,用逗号运算符构建,而不是像 C 中那样特殊的东西]

perldoc perlop:

Note that just as in C, Perl doesn't define when the variable is incremented or decremented. You just know it will be done sometime before or after the value is returned. This also means that modifying a variable twice in the same statement will lead to undefined behavior. Avoid statements like:

    $i = $i ++;
    print ++ $i + $i ++;