有没有一种方法可以在Perl中强制使用void上下文?

7

我只是出于极客的好奇心想知道Perl如何工作以及你可以用这种方式走多远。

有一些函数写成在三个上下文中的每一个上下文都表现得不同。

以下代码作为非常简单的示例:

use 5.012;

say context();
say scalar context();

sub context {
    if (wantarray) {
        return 'list';
    } elsif (defined wantarray) {
        return 'scalar';
    } else {
        return 'void'; # Destined to be discarded
    }
}

输出:

list
scalar

你能想到一种方法来触发第三个输出voidsay吗?此时需要调用context()

我知道这似乎是个矛盾,因为void context可能意味着你并没有真正返回或分配任何东西。但是根据我从阅读Perl工作原理所了解的,它并不是关于什么都没有返回,而是返回值在执行后被丢弃了。

所以,我想知道:当你恰好处于列表或标量上下文时,是否有一种方式可以像强制列表或标量上下文一样,强制使其进入void上下文?

3个回答

10
sub void(&) { $_[0]->(); () }

say        context();
say scalar context();
say void { context() };

更高级的代码可以给我们提供更好的语法:

use syntax qw( void );

say        context();
say scalar context();
say void   context();

顺便提一下,以下内容表明scalar不太像函数,而更像是编译时指令:

$ diff -u0 \
   <( perl -MO=Concise,-exec -Msyntax=void -E'say        f()' 2>&1 ) \
   <( perl -MO=Concise,-exec -Msyntax=void -E'say scalar f()' 2>&1 )
--- /dev/fd/63  2014-08-17 12:34:29.124827443 -0700
+++ /dev/fd/62  2014-08-17 12:34:29.128827401 -0700
@@ -7 +7 @@
-6  <1> entersub[t6] lKS/TARG    <-- "l" for list context
+6  <1> entersub[t7] sKS/TARG    <-- "s" for scalar context

同样适用于use syntax qw( void )中的void关键字:

$ diff -u0 \
   <( perl -MO=Concise,-exec -Msyntax=void -E'say        f()' 2>&1 ) \
   <( perl -MO=Concise,-exec -Msyntax=void -E'say void   f()' 2>&1 )
--- /dev/fd/63  2014-08-17 12:34:41.952692723 -0700
+++ /dev/fd/62  2014-08-17 12:34:41.952692723 -0700
@@ -7 +7 @@
-6  <1> entersub[t6] lKS/TARG    <-- "l" for list context
+6  <1> entersub[t6] vKS/TARG    <-- "v" for void context

如何使用use syntax qw( void );

Syntax::Feature::VoidVoid.xs才是真正发挥作用的部分,以下是其关键行:

STATIC OP* parse_void(pTHX_ GV* namegv, SV* psobj, U32* flagsp) {
    return op_contextualize(parse_termexpr(0), G_VOID);
}

STATIC OP* ck_void(pTHX_ OP* o, GV* namegv, SV* ckobj) {
    return remove_sub_call(o);
}

BOOT: {
    const char voidname[] = "Syntax::Feature::Void::void";
    CV* const voidcv = get_cvn_flags(voidname, sizeof(voidname)-1, GV_ADD);
    cv_set_call_parser(voidcv, parse_void, &PL_sv_undef);
    cv_set_call_checker(voidcv, ck_void, &PL_sv_undef);
}
  1. 使用 get_cvn 声明了一个名为 sub void 的子程序。(该子程序从未被定义。)Void.pm 中的代码将把该子程序导出到调用词法作用域。

  2. 使用 cv_set_call_parser,告诉 Perl 调用 void 遵循用户定义的语法。

  3. 使用 cv_set_call_checker,告诉 Perl 在编译后需要操作 void 的调用。

  4. 当 Perl 遇到对 void 的调用时,用户定义的解析器使用 parse_termexpr 提取一个项,然后使用 op_contextualize 更改该项的上下文为 void

  5. 之后,检查器从操作码树中删除对 void 的调用,同时留下其参数(该项)。


1
我非常有兴趣阅读那段代码。当然,我不能要求您花时间在没有其他目的的事情上,只是为了满足我对Perl更多奇怪角落的渴望。因此,如果您因为可以而想做它,我希望您能向我展示。感谢您的回答。 :-) - Francisco Zarabozo
1
@Francisco Zarabozo,Syntax::Feature::Void 已上传至 CPAN。如果尚未出现在您喜欢的镜像站点上,它很快就会出现。(最多一天时间) - ikegami
除了一些行之外,它和我早期的Syntax::Feature::Loop基本相同,所以没有困难。在我的答案中添加了它的工作原理的说明。 - ikegami

2

您必须确保函数的返回代码绝对不会被使用,例如

context();
1;

当不需要返回任何内容时(即!defined wantarray),使用return 'void'是没有意义的,因为这个返回值将不会被使用。

嗯,那很合乎逻辑。但问题是:除了实际处于void上下文中之外,是否有一种强制void上下文的方法? - Francisco Zarabozo
你可以强制一个函数处于void上下文中,但当它不在void上下文中时,你不能让它相信它在void上下文中。这样做没有太多意义。 - Steffen Ullrich
你会说这个陈述在任何可能的情况下都是绝对正确的吗? :-) - Francisco Zarabozo
2
我认为“在空上下文中”和“认为在空上下文中”之间没有区别,它只是一个标志。因此,我认为这个陈述是绝对正确的。但是人们可能会惊讶于哪些上下文是空的,哪些只是乍一看似乎是空的。 - Steffen Ullrich

1

你实际在问什么

引用 man perldata

当您使用use warnings pragma或Perl的-w命令行选项时,您可能会看到关于“无用常量或函数在“void context”中使用的警告。Void context表示值已被丢弃,例如仅包含"fred";getpwuid(0);的语句。对于关心自己是否在列表上下文中调用的函数,它仍然计数为标量上下文。

用户定义的子例程可以选择在void、scalar或list context中被调用时,是否关心它们的调用。大多数子例程不需要操心,因为标量和列表都会自动插入列表中。有关如何动态区分函数的调用上下文,请参阅 wantarray

因此,您正在询问的是:如果在列表或标量上下文中执行调用,则是否可能完全丢弃函数调用的值。

答案是肯定的!

在标量上下文中,仅使用列表的最后一个元素,其他元素在空上下文中计算。更确切地说,“列表”实际上从未是列表。有关逗号运算符在标量上下文中的行为的说明,请参见man perlop;在man perlfunc的描述部分末尾也用其他方式解释了这一点。最后,perldoc -f scalar也提到了这一点。
在列表上下文中,没有这种直接的方法。您需要使用与上述相同的技巧来获取任意数量(最好为0)的标量,然后摆脱它,以便它不影响您的列表内容。空列表重复就是您要找的东西。(顺便说一句,重复运算符在标量上下文中计算其第二个操作数。)
sub test_context() {
    wantarray and die "list\n";
    defined wantarray and die "scalar\n";
    die "void\n";
}
$\ = "\n"; # to make output prettier
### Uncomment the one you want to test.
# -- Somewhat canonical examples of contexts
#[test_context]; # list (+ warning of class 'void')
#print test_context; # list
#scalar(test_context); # scalar (forces scalar context anywhere)
#my $x = test_context; # scalar
#test_context; # void
#
# -- Examples of forcing void context
# Replace test_context with a fixed scalar and try again to see that even if
# the function returned a value, it would get discarded. Ignore the 'void' warning.
#print my $x = (test_context, 42);
#print '^', () x (test_context, 0), '$';

注意:不能使用void()

您无法创建一个类似于scalarvoid函数。

sub void {
    ();
}
print void(test_context);

这将导致test在列表上下文中被调用,因为函数参数始终在列表上下文中计算,除非在原型中另有说明。而且原型不能强制void上下文。
你只能通过改变Perl的语法来实现这样的事情,这是可能的,但相当复杂。
使用默认的Perl语法可以得到最好的近似值,如ikegami's answer所示。
为什么要这样做?
我认为这个问题纯粹出于好奇心,也许是想更好地理解Perl上下文。在我看来,它没有任何实际用途。正如perldoc -f wantarray中的示例所示,表示void上下文的未定义返回值旨在用于加速计算,例如如果可以在不生成输出的情况下执行副作用,则避免生成输出。
return unless defined wantarray; # don't bother doing more
my @a = complex_calculation();
return wantarray ? @a : "@a";

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接