理解上下文

上下文是Perl中最重要的概念之一,不理解上下文,就谈不上Perl风格的代码和Perl的技巧。

Perl有很多种上下文,但不幸的是没有一种规范去概括描述什么情况下处于什么上下文,学习和使用Perl的时候,需要记住几种常见的上下文环境,剩下的需要自己推理上下文。

最常见也是最重要的两种上下文是标量上下文和列表上下文:

  • 标量上下文表示此处需要的是一个标量数据
  • 列表上下文表示此处需要的是列表,即可能需要零个、一个或多个数据

常见的标量上下文包括:

  • 标量变量作为左值,例如$arr = xxx,赋值操作符左边的$表明这是一个标量上下文,赋值操作符右边需要提供标量数据
  • 加减乘除等算术运算符、大小比较、等值比较、字符串串联、字符串重复,等等,这些操作符要求标量上下文,需要标量数据
  • if、while的条件表达式部分,是标量上下文,严格来说是布尔标量上下文,要求布尔值
  • 操作数值、操作字符串的环境,需要标量上下文,例如正则匹配、正则替换、abs()取绝对值,等等

常见的列表上下文包括:

  • 数组变量作为左值,例如@arr = xxx,赋值操作符左边的@表明这是一个列表上下文,赋值操作符右边需要提供列表数据
  • hash变量作为左值,例如%p = xxx,其中%表明这是一个列表上下文,赋值操作符右边需要提供列表数据
  • print/say的参数,例如say 11,22,33;,各参数组成一个列表
  • for、foreach、each等迭代语句,需要列表上下文
  • 用于操作列表的函数,其某个参数需要列表上下文,例如grep、sort、map等列表类函数

当需要列表上下文时,如果提供的是非列表数据,则尽可能地转换为列表数据;同理,当需要标量上下文时,如果提供的是非标量数据,则尽可能地转换为标量数据

Perl有时候会自动隐式地将数据转换为符合上下文环境的数据,但如果无法进行隐式转换,那么需要显式强制转换。

为列表上下文提供列表数据

提供列表数据的一种方式是使用列表字面量:小括号包围或者qw()都可以。

my @arr = (11,22,33);  # 右边的括号是一个列表
my @arr = qw(a b c);   # 右边的qw()是一个列表
my ($a, $b, $c) = (11, 22, 33); # 两边的括号都是列表

如果没有使用列表字面量作为列表数据,那么Perl会自动隐式地将所提供的数据转换为列表。转换规则如下:

  • 标量转换为列表,并且标量数据作为列表的元素
  • 数组各元素直接作为列表各元素,即相当于数组压平后放入列表
  • hash的key和value都作为列表元素,但注意,转换时的元素顺序是未知的

例如,print/say函数的参数是一个列表上下文:

my @arr = (11,22,33);
my %person = (name=>"junma", age=>23,gen=>"male");

say 11,22,33; # 各标量转换为列表 [11 22 33]
say @arr;     # 数组转换为列表 [11 22 33]
say %person;  # hash转换成列表 [name junma age 23 gen male]

# print/say使用标量$,的值作为分隔符输出各列表元素
# 输出结果:
#   112233
#   112233
#   genmaleage23namejunma

以上这些转换为列表的方式是隐式的,还可以使用小括号包围它们来显式地、强制地转换为列表。例如:

my @arr = 33;    # 隐式转换
my @arr = (33);  # 显式转换

my @arr = @arr0;   # 隐式转换
my @arr = (@arr0); # 显式转换

my @arr = %p;     # 隐式转换
my @arr = (%p);   # 显式转换

有些场景下,必须使用小括号显式地转换为列表,否则会产生歧义。例如:

# 改成qw(@arr,44,55)也可以
# 但如果去掉小括号,将报错
foreach $i (@arr,44,55) {
  say $i;
}

在这里需要理解一种比较特殊的用法:括号里放空括号或者括号里放空数组,不会产生任何效果。这种现象的原因很容易理解,因为列表上下文中,内部列表或内部数组的元素直接作为外部列表的元素,内部使用空列表或空数组,没有为外部列表提供任何元素。

(())        # 等价于()
((),(),())  # 等价于()

my @arr = ();
(@arr,(),@arr); # 等价于()

my @a = ((),(),()); # 等价于@a = ()
say scalar @a;      # 输出:0

为标量上下文提供标量数据

如果在标量上下文处提供的是字符串、数值、引用等本就是标量的数据,则直接使用它们。

如果提供的是数组或者hash,则perl会尝试自动隐式地将它们转换为标量类型。转换规则如下:

  • 数组转换为标量得到的是数组的长度
  • hash转换为标量得到的是M/N格式的字符串,N表示hash的容量,M表示hash当前的键值对数量

例如,加法运算要求标量上下文,如果加法操作的两个操作数不是标量,则会隐式转换成标量。

my @arr = (11,22,33,44);

# 下面的加法操作会将操作数@arr转换为标量
# 得到arr数组的长度4
say 1+@arr;    # 5,等价于1+4

转换为标量数据是非常常见的需求,如果当前身处非标量上下文,但却需要标量数据,那么可以采取一些简短的技巧改变上下文环境,或者使用scalar显式地、强制地转换为标量。例如:

my @arr = (11,22,33,44);

# 使用加0操作得到标量环境且不影响标量结果
say @arr+0;

# 使用双波浪号 ~~ 转换为标量上下文,
# 单个~是位取反操作,要求标量数据,两次位取反得到原数据的标量形式
say ~~@arr;

# 使用scalar强制转换为标量数据
say scalar @arr;

标量上下文中的列表

上面介绍了在标量上下文中,数组和hash会按照怎样的规则转换成标量数据。但是,标量上下文中,列表如何转换成标量数据呢?

例如:

# 左边表示标量上下文,右边给了列表数据
my $a = (11,22,33);
my $b = qw(11 22 33);
say $a, "-", $b;   # 输出:33-33

从输出结果来看,变量a和b的值都是33,它们正好都是列表的最后一个元素。

也就是说,标量上下文中的列表,会返回最后一个列表元素作为标量数据,其他元素被丢弃

say "h".(1,2,3,4);  # 输出:h4
say ~~(11,22,33);   # 输出:33


my @arr = (11,22,33);
@arr = @arr + (55,66); # 等价于@arr=3+66
say "@arr";         # 输出69

实际上,当遇到标量上下文中的列表时,会从前向后逐步评估每一个元素,然后丢弃这个元素的评估结果,直到评估完最后一个元素返回它。

# ($a = 3, $b = 4)先为a赋值3,丢弃它,再为b赋值4
# 返回$b,并将$b赋值给c
my $c = ($a = 3, $b = 4);
say "$a-$b-$c";   # 输出:3-4-4

另外,在一些书籍、文档(包括官方手册)上,将标量上下文中的列表解释为逗号操作符的效果,这也可以。这是因为列表总是表现为逗号分隔各元素、必要的时候使用一个小括号包围的形式(qw列表字面量在编译后也会转换成这种小括号形式),而逗号是一个操作符,它的作用是评估左边的表达式,然后丢弃,继续评估右边的表达式,直到评估完最后一个表达式,返回它的结果。

# 右边先丢弃1,再丢弃2,返回3
my $a = (1,2,3);

my $a=3,$b=4;

在这里也有一个比较特殊的用法需要理解:括号里(或列表中)使用连续逗号,不会产生任何效果

my $a=3,$b=4,,,,;      # 等价于$a=3,$b=4
my @a = (1,,,,3,2,,);  # 等价于(1,3,2)
((3,,),,);              # 等价于((3)),等价于(3)

空上下文

当表达式或语句的返回结果不被使用时,它们就处于空上下文(void context)。换句话说,空上下文会丢弃表达式或语句的结果。

例如,下面几种情况都在空上下文中:

# 直接写字面量、变量、表达式等,
# 这些数据不被使用,直接丢弃而不会报错
333;
"abc";
3+3;
$name;

# 赋值语句的返回结果被丢弃
my $a = 33;

# 函数调用的结果被丢弃
abs(-1);

理解上下文后的一些赋值技巧

将Perl的赋值操作和其他编程语言的赋值操作进行对比,会发现其他语言的赋值操作都规规矩矩的,就是简简单单的赋值语句。

但是Perl的赋值语句非常灵活,灵活到我学习的时候感觉很苦恼,因为初学Perl的时候,这些赋值语句都是像黑科技一样的技巧,没有【规律】可言,也不知道到底还有多少种赋值技巧没有被发现。这些技巧几乎是Perl独有的技巧,不适用于其他语言。这可能是某些人攻击Perl的其中一个理由:Perl太过灵活,而且缺乏规范。

当我对Perl的细节理解逐渐深入后,我才明白,这些技巧都有理可依,且是对语法规则的深入应用。以前不理解这些技巧,只是因为像学其他语言的赋值语句一样,简简单单地学了基本赋值规则。

下面是一些结合上下文后的赋值技巧,我无法总结全部,只能总结一些常见的技巧。

## 1.标量上下文中的列表:得到最后一个元素
##   赋值语句返回标量左值
$a = (11,22);   # a=22,赋值语句返回22
$a = $b = (11,22); # a和b都等于22
# 下面func()最后返回空列表,标量上下文的赋值返回0,布尔假
while (($a, $b) = func()){} 

# 赋值语句返回的标量左值可被修改
chomp ($str = "hello\n"); # 将会去除$str中的换行符
($a = $b) =~ s/xxx/yyy/;  # 将会修改变量a
($a += 2) *= 2;           # 将会对a加2后乘2


## 2.列表上下文中的列表:逐个赋值
##   左边多的变量被赋值为undef,右边多的元素被丢弃
##   赋值语句本身的返回值:
##     在标量上下文:返回右边列表的长度
##     在列表上下文:返回左边列表
($a, $b) = (11,22);  # 赋值语句返回2或(11,22)
($a) = (11,22);      # 赋值语句返回2或(11)
() = (11,22);        # 赋值语句返回2或()
($a,$b,$c) = (11,22);  # 赋值语句返回2或(11,22,undef)
($a,undef,$c) = (11,22,33); # 赋值语句返回3或(11,undef,33)

$num = () = <>;     # 将已读取的行数赋值给num
$num = @arr = <>;   # 同上
@arr=($a,$b)=(11,22,33);  # @arr=(11,22),$a=11,$b=22