Ruby Array #count如何处理多个块参数

时间:2019-01-07 00:53:53

标签: ruby

当我执行以下操作时:

[[1,1], [2,2], [3,4]].count {|a,b| a != b} # => 1

将块参数ab分别分配给每个内部数组的第一个和第二个值。我不明白这是如何实现的。

文档中针对Array#countEnumerable#count带有块的唯一示例使用单个块参数:

ary.count {|x| x % 2 == 0} # => 3

2 个答案:

答案 0 :(得分:5)

就像任务一样,有一个(不是这样)秘密的快捷方式。如果右侧是一个数组,而左侧有多个变量,则该数组将被展开,因此以下两行相同:

a, b, c = [1, 2, 3]
a, b, c = *[1, 2, 3]

当产生的值是一个数组并且有多个参数时,虽然块不是同一块,但是块具有相同的纹理。因此,当您yield [1, 2, 3]时,这两个块的作用相同:

do |a, b, c|
  ...
end

do |(a, b, c)|
  ...
end

因此,在您的情况下,该值被解构,就像您编写此代码一样:

[[1,1], [2,2], [3,4]].count {|(a,b)| a != b} # => 1

如果要与数组一起传递另一个值,则必须明确指定结构,因为数组的解构不会像我们想要的那样自动进行:

[[1,1], [2,2], [3,4]].each.with_index.count {|e,i| i + 1 == e[1] }
# automatic deconstruction of [[1,1],0]:
# e=[1,1]; i=0

[[1,1], [2,2], [3,4]].each.with_index.count {|(a,b),i| i + 1 == b }
# automatic deconstruction of [[1,1],0], explicit deconstruction of [1,1]:
# a=1; b=1; i=0

[[1,1], [2,2], [3,4]].each.with_index.count {|a,b,i| i + 1 == b }
# automatic deconstruction of [[1,1],0]
# a=[1,1]; b=0; i=nil
# NOT what we want

答案 1 :(得分:4)

  

我看过Array.count和Enumerable.count的文档,并且给出的带有块的唯一示例使用单个块参数...

Ruby与几乎所有主流编程语言一样,不允许用户代码更改语言的基本语义。换句话说,您不会在Array#count的文档中找到有关块形式参数绑定语义的任何信息,因为块形式参数绑定语义是由Ruby Language Specification指定的,Array#count不可能对此进行更改。

  

我不明白这是如何实现的。

这与Array#count无关。这只是块形式参数的标准块形式参数绑定语义。

块形式参数的形式参数绑定语义不同于方法形式参数的形式参数绑定语义。特别是,它们在处理形式参数和实际参数之间的不匹配方面更加灵活。

  • 如果恰好有一个块形式参数,而您yield多于一个块实际参数,则该块形式参数将绑定到包含该块实际参数的Array上。
  • 如果有多个块形式参数,而您yield正好一个块实际参数,并且一个实际参数是Array,则块形式参数将绑定到Array的各个元素。(这就是您在示例中看到的内容。)
  • 如果您的yield块实际参数多于该块具有形式参数的块,则多余的实际参数将被忽略。
  • 如果传递的实际参数少于该块具有形式参数的实际参数,则这些多余的形式参数已定义但未绑定,并求值为nil(就像已定义但统一的局部变量一样)。

如果仔细观察,您会发现块形式参数的形式参数绑定语义更接近赋值语义,即,您可以想象一个赋值操作符左侧带有块形式参数的赋值,并且阻止在右侧的实际参数。

如果您具有如下定义的块:

{|a, b, c|}

,并yield像这样:

yield 1, 2, 3, 4

您几乎可以想象到块形式参数绑定是这样工作的:

a, b, c = 1, 2, 3, 4

如果您的问题中有这种情况,那么您有一个这样定义的块:

{|a, b|}

,并yield像这样:

yield [1, 2]

您几乎可以想象到块形式参数绑定是这样工作的:

a, b = [1, 2]

如您所知,当然会有以下结果:

a #=> 1
b #=> 2

有趣的事实:在Ruby 1.8之前,使用 actual 分配来阻止形式参数绑定 !例如,您可以将常量,实例变量,类变量,全局变量甚至属性writer(!!!)定义为形式参数,然后yield进入该块时, Ruby 按字面意思执行任务:

class Foo
  def bar=(value)
    puts "`#{__method__}` called with `#{value.inspect}`"
    @bar = value
  end

  attr_reader :bar
end

def set_foo
  yield 42
end

foo = Foo.new

set_foo {|foo.bar|}
# `bar=` called with `42`

foo.bar
#=> 42

疯了吧?

这些块形式参数绑定语义的最广泛应用是使用Hash#each(或使用Enumerable实例作为接收者的任何Hash方法)时。 Hash#each方法yield是一个单一的包含两个元素的Array,其中包含键和值作为该块的实际参数,但是我们几乎总是将其视为{{1} }将键和值作为单独的实际参数。通常,我们更喜欢写

yield

hsh.each do |k, v|
  puts "The key is #{k} and the value is #{v}"
end

这完全等同于您在问题中看到的内容。我敢打赌,您从未问过自己,为什么只将hsh.each do |key_value_pair| k, v = key_value_pair puts "The key is #{k} and the value is #{v}" end 传递给一个Hash#each,为什么可以将带有两个块形式参数的块传递给yield?嗯,这种情况是完全相同的。您正在将具有两个块形式参数的块传递给方法,该方法Array每次迭代yield