【问题标题】:How do I read two items at a time in a Perl foreach loop?如何在 Perl foreach 循环中一次读取两个项目?
【发布时间】:2009-02-20 14:41:28
【问题描述】:

我正在寻找的是这样的:

@list = qw(1 2 3 4 5 6);
foreach (@list) {
  #perl magic goes here 
  print "i: $i, j:$j\n";
}

返回:

i:1, j:2
i:3, j:4
i:5, j:6

作为对下面一个非常好的建议的回应,我需要指定此脚本将在其他人的构建服务器上运行,并且不允许我使用来自 CPAN 的任何模块。仅限标准 Perl。

【问题讨论】:

  • 我想指出,在 Perl 6 中它是 for @list -> $i, $j { say "i: $i, j:$j" }
  • @BradGilbert Addictive ,我们可以预测这将进入 Perl5 吗? :-) 另见Is it possible to assign two variables in Perl foreach loop?
  • 不允许使用任何来自 CPAN 的模块?为什么不? Yes, even you can use CPAN!
  • 底部是一些解决问题的方法。测试其中一些,并检查大源阵列的 RAM 利用率。顺便提一句。将数组转换为 has 并不是一个好主意。

标签: perl loops


【解决方案1】:

我相信正确的方法是使用 natatime,来自List::MoreUtils

来自文档:

natatime 块列表

创建一个数组迭代器,用于在 $n 项的块中循环数组 一次。 (一次n,明白吗?)。一个例子可能更好 比我能用语言给出的解释。

例子:

 my @x = ('a' .. 'g');
 my $it = natatime 3, @x;
 while (my @vals = $it->())
 {
     print "@vals\n";
 }

打印出来

a b c 定义 G

List::MoreUtils::natatime的实现:

sub natatime ($@)
{
    my $n = shift;
    my @list = @_;

    return sub
    {
        return splice @list, 0, $n;
    }
}

【讨论】:

  • 这太棒了。任何东西都比 c 风格的 for 循环好,但这要好得多。
  • 正如其他评论所述,这是“非常糟糕”的答案之一:“抓住一个模块”;--) 所以要完整:使用 List::MoreUtils qw(natatime);我的@list = qw(1 2 3 4 5 6);我的 $it = natatime 2, @list; while(my($i, $j)= $it->()) { print "i: $i, j:$j\n"; }
  • 您不必在系统范围内安装它。您可以在本地复制它,甚至只是获取代码(查看cpansearch.perl.org/src/VPARSEVAL/List-MoreUtils-0.22/lib/List/…),它非常短!
  • 请注意,foreach 和 natatime 之间有一点区别。 foreach 更快,内存消耗更少(不会像 natatime 那样在内部复制数组)并允许修改数组成员,例如 foreach my $a (@x) { $a++ } 与 $_++ foreach @x 相同
  • natatime 如果这样做就不需要复制列表:my $list = \@_;然后稍后:拼接@$list, 0, $n;
【解决方案2】:

我会使用拼接。

my @list = qw(1 2 3 4 5 6);
while(my ($i,$j) = splice(@list,0,2)) {
  print "i: $i, j: $j\n";
}

【讨论】:

  • 如另一个答案所述,这也是破坏列表的。非破坏性答案将涉及带有索引的 for 循环并执行 index + 1。
  • 这也是我的选择,如果我不关心销毁列表的话。 +1。
  • 如果列表破坏有问题,您可以事先复制列表。您甚至可以在 { block } 内执行此操作,因此完成后它会超出范围: { my @temp = @list; while(my ($i, $j) = splice(@list,0,2)) { print "i: $i, j: $j\n"; } }
  • @ChrisLutz ++ 正如@mirod 指出的那样,List::MoreUtils natatime 也是这样实现的。
【解决方案3】:

我认为您希望以不同的方式执行此操作。 试试这个:

while (scalar(@list) > 0) {
    $i = shift(@list);
    $j = shift(@list);
    print "i: $i, j:$j\n";
} 

请记住,这会破坏列表,但它适用于那个小循环。

【讨论】:

  • 这是破坏列表的。对他的要求有很大的不同
  • @cmartin,他的请求中没有任何内容表明它不能用完列表。因此,清单破坏性行动可能符合要求。但是,如果是这样的话,就没有理由换班,只需拼接。
  • 我没有测试,但是当@list 中有奇数个元素时,第二个班次不会失败吗?但是朋友之间有什么奇怪的undef。 ;-)
  • 这与其他示例一样具有破坏性,但您应该通过 (scalar(@list)>1) 对其进行更正,以防止获取最后一个未配对的元素。
【解决方案4】:

设置一些测试数据,并导入say

use Modern::Perl;
use List::AllUtils qw'zip';

my @array = zip @{['a'..'z']}, @{[1..26]} ;

使用增量变量的简单循环。

    {
      my $i = 0;
      while(
        (my($a,$b) = @array[$i++,$i++]),
        $i <= @array # boolean test
      ){
        say "$a => $b";
      }
    }

使用 List::Pairwise  (pair) 循环对。

    use List::Pairwise qw'pair';

    for my $pair (pair @array){
      my($a,$b) = @$pair;

      say "$a => $b";
    }

一次循环遍历数组 2,使用 List::MoreUtils  (natatime)

    use List::AllUtils qw'natatime';

    my $iter = natatime 2, @array;
    while( my($a,$b) = $iter->() ){
      say "$a => $b";
    }

将其强制转换为哈希,然后循环遍历键。如果您不关心订单,这很有用。

    {
      my %map = @array;
      for my $key (keys %map){
        my $value = $map{$key};
        say "$key => $value";
      }
    }

【讨论】:

  • 如果您从数组中成对地注入子例程 @array = qw/michael tan kelly wong/sub aFunc($a, $b){say $a, $b}aFunc(@$_) for (pair @array),则很有用
  • 核心模块List::Util中新增了pair函数。
【解决方案5】:

不幸的是,最接近的等价物是老派:

for(my $ix = 0; $ix <= $#list; $ix += 2) {
    my $i = $list[$ix];
    my $j = $list[$ix + 1];
    print "i: $i, j:$j\n";
}

我更喜欢 Jack M 的答案,真的,虽然我会用更性感的 Perl 来写:

while(@list) {
    my $i = shift @list;
    my $j = shift @list;
    print "i: $i, j:$j\n";
}

【讨论】:

  • +1 用于标准 for() 循环。一些控制结构是经典的,因为它们有效 =)。
  • @jrockway,他将 $ix
  • @chaos - 来自@jrockway 的编辑评论,更改不是为了语法高亮,而是为了可读性和“惯用性”。它的工作原理相同,并且少了两个标点符号。你怎么能反驳呢? :-)
  • 他说这是为了“修复降价标记”,这有点不清楚,因为没有涉及降价,但似乎与它有任何关系的最近的事情是 # 是解释为评论。
  • 我从来没有真正使用 $#array 来获取数组的最后一个元素。所以我更喜欢 $ix
【解决方案6】:

如果我只能使用没有模块的标准 Perl,我可能会使用 C 风格的 for 循环,计数为 2:

for( my $i = 0; $i &lt; @array; $i += 2 ) {
    my( $j, $k ) = @array[ $i, $i+1 ];
    ...
    }

如果您有奇数个元素,则必须决定如何处理额外的元素。你的问题可能不在乎你得到一个未定义的额外元素,因为你特别需要对。

仅仅读取数组末尾并不会改变数组,所以这部分没问题。

如果你必须有对,一个简单的策略可能是在数组的末尾添加一个适当的值,这样你总是以对结束。同样,您可以删除最后一个元素(或任何元素)以再次以偶数结束。这些取决于您的问题。

否则,你做的工作会稍微多一点:

for( my $i = 0; $i < @array; $i += 2 ) {
    push @pair, $array[$i];
    push @pair, $array[$i+1] if $i+1 <= $#array;
    ... 
    }

但是,如果您想从其中一个无法使用的模块中获得一些花哨的东西,您可以将该模块添加到您的代码中。如果您可以编写代码,则可以使用模块。您可能只需要在正确设置@INC 时将模块包含在您提供的所有代码中。这就是inc::Module::InstallPAR的基本思想。

我花费大量时间使用构建系统来创建自己的 CPAN 存储库,从其私有 CPAN 安装其依赖项,然后测试代码。拥有一个构建农场并不排除使用模块。这是当地的政策。但是,即使有可能,这也不一定适用于所有情况。

【讨论】:

  • 对于数组中有奇数元素的情况,它将获得最后一个未配对的元素,并且作为第二个值,这会将 undef 添加到配对中。
【解决方案7】:

冒着死灵术标签的风险,我决定从 Tim Toady 的背包中再添加一个:

for (0 .. $#list) {
    next if $_ % 2;
    my ($i, $j) = @list[$_, $_ + 1];
    say "i:$i, j:$j";
}

非破坏性、无重复列表、无状态变量且相当简洁。

【讨论】:

  • 嗯,我得到了死灵法师的标签,8年后回答! :-)
【解决方案8】:

您可能希望创建一个简单的子例程以使其适合您。

我建议这样做:

{
  my $cl_ind = 0;
  sub arrayeach(@) {
    my @obj = @_;
    if(($cl_ind+2) > @obj)
    {
      $cl_ind = 0;
      return;
    }
    $cl_ind+=2;
    return ($obj[$cl_ind-2],$obj[$cl_ind-1]);
  }
}

闭包让它干净利落。使用 arrayeach (它的工作方式类似于 hash each 而不需要对数组进行危险的强制:

my @temp = (1,2,3,4,5,6,1,2,3,4,5,6);
while( ($a,$b) = arrayeach(@temp)) {
  print "A $a AND $b\n";
}

这是非破坏性的。

【讨论】:

  • 如果我想并行消耗两个数组会发生什么?
  • 这不是问题所在。但很高兴看到我有比通常被认为“更糟糕”的解决方案更多的负面观点。
【解决方案9】:

通用功能解决方案怎么样。

use Carp; # so mapn can croak about errors

sub mapn (&$@) {
    my ($sub, $n, @ret) = splice @_, 0, 2;
    croak '$_[1] must be >= 1' unless $n >= 1;
    while (@_) {
        local *_ = \$_[0];
        push @ret, $sub->(splice @_, 0, $n)
    }
    @ret
}

sub by ($@) {mapn {[@_]} shift, @_}
sub every ($@); *every = \&by;

mapn 函数的工作方式与map 类似,不同之处在于其块之后的第一个参数是要采用的元素数。它将第一个元素放在 $_ 中,并将所有元素放在 @_ 中。

print mapn {"@_\n"} 2 => 1 .. 5;
# prints
1 2
3 4
5

接下来的两个相同的子句 byevery 为各种循环结构创建有用的副词。他们使用 mapn 处理列表,并返回所需大小的数组引用列表

print "@$_\n" for every 2 => 1..10;

print map {"@$_\n"} grep {$_->[1] > 5} by 2 => 1..10;

我发现这是一种比 natatime 或其他一次性解决方案(如 c 风格的 for 循环)更清洁、更直观的解决方案。

【讨论】:

  • 这真是太好了——我喜欢简短的“副词”类型子例程的外观和感觉,这些子例程链接成这样的简短但功能强大的语句/表达式。感谢List::Gen - 它为大众带来了时尚的力量;-)
  • 只有一个错误。当我们分配 %hash=@array 时,最后一个未配对的元素将被忽略。在您的解决方案中,这将与添加的 undef 一起使用。
【解决方案10】:
my $i;
for ( qw(a b c d) ) {
    if (!defined($i)) { $i = $_; next; }
    print STDOUT "i = $i, j = $_\n";
    undef($i);
}

输出:

i = a, j = b
i = c, j = d

它也适用于列表,而不仅仅是数组。

【讨论】:

  • undef($i) 是什么?只需将其包装在 do {} 中,然后我的 $i 将是局部变量。
【解决方案11】:

正如 Mirod 解释的那样,它没有太多代码。这几乎就是你需要的一切。 (请注意,我没有对奇数列表等进行任何检查。)

#!/usr/bin/env perl
use strict;
use warnings;

my @list = qw/1 2 3 4 5 6/;
my $get_em = get_by(2, @list);

while ( my ($i, $j) = $get_em->() ) {
  print "i: $i, j: $j\n";
}

sub get_by {
  my $n = shift;
  my @list = @_;

  return sub {
    return splice @list, 0, $n;
  }
}

【讨论】:

    【解决方案12】:

    小型数组的快速解决方案:

    for ( map {$_*2} 0..@list/2-1 ){
        my ($i, $j) = @list[$_,$_+1];
        print "i: $i, j:$j\n";
    }
    

    某种单线器

    数据:

    @v = (a=>1, b=>2, c=>3);
    

    这个

    print join ', ', map{sprintf '%s:%s', $v[$_], $v[$_+1]} grep {!($_%2)} 0..$#v
    

    或类似的东西

    print join ', ', map {sprintf '%s:%s', @v[$_,$_+1]} map {$_*2} 0..@v/2-1;
    

    结果是一样的

    a:1, b:2, c:3
    

    【讨论】:

      【解决方案13】:

      使用 for 循环可以满足您的需要。

      use strict;
      use warnings;
      
      my @list = qw(1 2 3 4 5 );
      my $i = 0;
      
      for ($i = 0; $i < scalar(@list); $i++)
      {
          my $a = $list[$i];
          my $b = $list[++$i];
          if(defined($a)) {
              print "a:$a";
          }
          if(defined($b)) {
              print "b:$b";
          }   
          print "\n";
      }
      

      编辑:我更正了我的帖子以使用标量函数来检索数组的大小,并添加一些检查以防数组不包含偶数个元素。

      【讨论】:

      • 您不需要使用标量,因为比较器在标量上下文中获取参数。那么 \@list 会自动转换成数组元素的个数。
      【解决方案14】:

      另一种方法,不完全干净,但可用。每个创建迭代器,您可以使用它两次。当参数为经典数组时,返回索引和值,请阅读:https://perldoc.perl.org/functions/each.html

      所以,你的代码可以是这样的:

      my @array=qw(one two three four five); #five element as unpaired will be ignored
      while (my ($i1,$one,$i2,$two)=(each(@array),each(@array)) {
        #we will use $ix for detect end of array
        next unless defined $i1 and defined $i2; #secure complete end of array
        print "fetched array elements: $one => $two\n";
      };
      

      上面的示例不会破坏源数据,反对移位或类似的。 我希望这对任何人都有帮助。当然,使用普通迭代器的情况要好得多。

      【讨论】:

        【解决方案15】:

        我想出了这段代码来解决类似的要求:

        sub map_pairs(&\@) {
            my $op = shift;
            use vars '@array';
            local *array = shift;    # make alias of calling array
        
            return () unless @array;
        
            # Get package global $a, $b for the calling scope
            my ($caller_a, $caller_b) = do {
                my $pkg = caller();
                no strict 'refs';
                \*{$pkg.'::a'}, \*{$pkg.'::b'};
            };
        
            # Get index counter size.
            my $limit = $#array/2;
        
            # Localize caller's $a and $b
            local(*$caller_a, *$caller_b);
        
            # This map is also the return value
            map {
                # assign to $a, $b as refs to caller's array elements
                (*$caller_a, *$caller_b) = \($array[$_], $array[$_+1]);
                $op->();    # perform the transformation
            } 
            map { 2 * $_ } 0..$limit;  # get indexes to operate upon.
        }
        

        你这样使用它:

        @foo = qw( a 1 b 2 c 3 );
        my @bar = map_pairs { "$a is $b" } @foo;
        

        得到:

        @bar = ( 'a is 1', 'b is 2', 'c is 3' );
        

        我一直想提交给 List::MoreUtils 的维护者,但我没有提供 XS 版本。

        【讨论】:

          【解决方案16】:

          这是一个不复制列表的 natatime 实现:

          sub natatime {
            my $n = shift;
            my $list = \@_;
          
            sub {
              return splice @$list, 0, $n;
            }
          }
          
          my $it = natatime(3, qw(1 2 3 4 5 6));
          while ( my @list = $it->() ) {
            print "@list\n";
          }
          

          【讨论】:

          • 这个函数会破坏源数组。当您用任何其他变量交换 qw(1 2 3 4 5 6) 时,您将丢失存储在其上的数据。
          • @Znik - 不,不会。试试看,或者举个例子说明你在说什么。
          【解决方案17】:

          这可以非破坏性地完成,Eric Strom's 简直太棒了List::Gen

          perl -MList::Gen=":utility" -E '@nums = "1" .. "6" ; 
                say "i:$_->[0] j:$_->[1]" for every 2 => @nums'
          

          输出

          i:1 j:2 
          i:3 j:4 
          i:5 j:6 
          

          编辑(添加无 CPAN 版本):

          数组切片和 C 样式 for 循环 à la brian d foyTom Christiansen!这可以理解为“使用索引($i)一次循环遍历@listforeach$n元素”:

          use v5.16; # for strict, warnings, say
          
          my @list = "1" .. "6";
          my $n = 2 ;   # the number to loop by
          $n-- ;        # subtract 1 because of zero index
          
          foreach (my $i = 0 ; $i < @list ; $i += $n ) { 
            say "i:", [ @list[$i..$i+$n] ]->[0], " j:", [ @list[$i..$i+$n] ]->[1];
            $i++ ;          
          }
          

          我们将结果作为匿名数组 ([ ]) 的元素 (-&gt;[0]) 访问。对于更通用的输出,可以单独使用插值数组切片,例如print "@list[$i..$i+$n]"; 根据需要更改 $n 的值。

          【讨论】:

            【解决方案18】:

            元素个数除以二,迭代一。打印索引和下一个:

            print "i:$list[$_*2], j:$list[$_*2+1]\n" for 0..$#list/2;
            

            或者如果你想要一个完整的块:

            foreach my $i ( 0..$#list/2 ) { print "i:$list[$i*2], j:$list[$i*2+1]\n"; }
            

            或者:

            foreach my $n ( 0..$#list/2 ) { 
                my ($i,$j) = @list[$n*2,$n*2+1];
                print "i:$i, j:$j\n"; 
            }
            

            【讨论】:

              【解决方案19】:

              我认为更简单的方法是使用旧的可怜的“每个”。 直接这样:

              while (my ($key,$value) = each @list) {
                      print "$key=$value\n";
              }
              

              更新:

              是的,这是错误的。应该先将列表转换为哈希,但它可能过于昂贵:

              my %hash = (@list);
              while (my ($key,$value) = each %hash) {
                      print "$key=$value\n";
              }
              

              【讨论】:

              • 很好地指出 each 与数组一起工作(从 perl-5.12 开始)但它返回“索引和值”,所以 perl -E '@n = "1" .. "6" ; while ( ($i, $j ) = each @n ) { say "i:$i j:$j" } 给出:i:0 j:1 ... i:5 j:6 而不是一次两个。你能想办法让你的帖子更正确吗?
              • 在本次更新中,奇数元素将被重复,仅保存一对。例如,对于数组:qw(一二一三),你会丢失一对,可能只有最后一对冲突会保存在这个哈希中。
              【解决方案20】:
              while ($#rec>0) {
                my($a,$b) = ( shift(@rec), shift(@rec) );
                print "($a,$b)\n";
              }
              

              【讨论】:

              • 虽然此代码 sn-p 可能是解决方案,但 including an explanation 确实有助于提高您的帖子质量。请记住,您是在为将来的读者回答问题,而这些人可能不知道您提出代码建议的原因。
              • @rec 与 shift 会混淆原始内容,尤其是在代码下游
              猜你喜欢
              • 1970-01-01
              • 2019-08-28
              • 2016-08-08
              • 2023-04-11
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2023-02-03
              相关资源
              最近更新 更多