Ruby 块采用数组或多个参数

Ruby block taking array or multiple parameters

今天我惊讶地发现 ruby 自动找到作为块参数给定的数组的值。

例如:

foo = "foo"
bar = "bar"
p foo.chars.zip(bar.chars).map { |pair| pair }.first #=> ["f", "b"]
p foo.chars.zip(bar.chars).map { |a, b| "#{a},#{b}" }.first #=> "f,b"
p foo.chars.zip(bar.chars).map { |a, b,c| "#{a},#{b},#{c}" }.first #=> "f,b,"

我原以为最后两个例子会给出某种错误。

  1. 这是 ruby 中更一般概念的示例吗?
  2. 我认为我在问题开头的措辞不正确,我怎么称呼这里发生的事情?

Ruby 的块机制有一个怪癖,那就是如果你正在迭代包含数组的东西,你可以将它们扩展到不同的变量中:

[ %w[ a b ], %w[ c d ] ].each do |a, b|
  puts 'a=%s b=%s' % [ a, b ]
end

这种模式在使用 Hash#each 时非常有用,并且您想分解成对的 keyvalue 部分:each { |k,v| ... } 在 Ruby代码。

如果您的块接受多个参数并且被迭代的元素是一个数组,那么它会切换参数的解释方式。你总是可以 force-expand:

[ %w[ a b ], %w[ c d ] ].each do |(a, b)|
  puts 'a=%s b=%s' % [ a, b ]
end

这对于事情更复杂的情况很有用:

[ %w[ a b ], %w[ c d ] ].each_with_index do |(a, b), i|
  puts 'a=%s b=%s @ %d' % [ a, b, i ]
end

因为在这种情况下它迭代数组 附加的另一个元素,所以每个项目实际上是一个内部形式 %w[ a b ], 0 的元组,它将如果您的块只接受一个参数,则转换为数组。

这与您在定义变量时可以使用的原则大致相同:

a, b = %w[ a b ]
a
# => 'a'
b
# => 'b'

这实际上给ab分配了独立的值。对比:

a, b = [ %w[ a b ] ]
a
# => [ 'a', 'b' ]
b
# => nil

Ruby 街区就是那样古怪。

规则是这样的,如果一个块接受了多个参数,并且它产生了一个响应 to_ary 的对象,那么该对象将被扩展。这使得生成一个数组与生成一个元组对于带有两个或更多参数的块的行为方式似乎相同。

yield [a,b]yield a,b 确实不同,尽管当块仅采用一个参数或当块采用可变数量的参数时。

让我来证明这两个

def yield_tuple
  yield 1, 2, 3
end

yield_tuple { |*a| p a }
yield_tuple { |a| p [a] }
yield_tuple { |a, b| p [a, b] }
yield_tuple { |a, b, c| p [a, b, c] }
yield_tuple { |a, b, c, d| p [a, b, c, d] } 

打印

[1, 2, 3]
[1] 
[1, 2]
[1, 2, 3]
[1, 2, 3, nil]

def yield_array
  yield [1,2,3]
end

yield_array { |*a| p a }
yield_array { |a| p [a] }
yield_array { |a, b| p [a, b] }
yield_array { |a, b, c| p [a, b, c] }
yield_array { |a, b, c, d| p [a, b, c, d] }

打印

[[1, 2, 3]]
[[1, 2, 3]] 
[1, 2] # array expansion makes it look like a tuple
[1, 2, 3] # array expansion makes it look like a tuple
[1, 2, 3, nil] # array expansion makes it look like a tuple

最后证明 Ruby 中的所有内容都使用 duck-typing

class A
  def to_ary
    [1,2,3]
  end
end

def yield_arrayish
  yield A.new
end

yield_arrayish { |*a| p a }
yield_arrayish { |a| p [a] }
yield_arrayish { |a, b| p [a, b] }
yield_arrayish { |a, b, c| p [a, b, c] }
yield_arrayish { |a, b, c, d| p [a, b, c, d] }

打印

[#<A:0x007fc3c2969190>]
[#<A:0x007fc3c2969050>]
[1, 2] # array expansion makes it look like a tuple
[1, 2, 3] # array expansion makes it look like a tuple
[1, 2, 3, nil] # array expansion makes it look like a tuple

PS,相同的数组扩展行为适用于 proc 闭包,闭包表现得像块,而 lambda 闭包表现得像方法。

I would have expected the last two examples to give some sort of error.

如果您从方法中传递 proc,它实际上会以这种方式工作。屈服于这样的过程要严格得多——它会检查它的元数并且不会尝试将数组参数转换为参数列表:

def m(a, b)
  "#{a}-#{b}"
end

['a', 'b', 'c'].zip([0, 1, 2]).map(&method(:m))
#=> wrong number of arguments (given 1, expected 2) (ArgumentError)

这是因为 zip 创建一个(数组的)数组,而 map 只生成每个元素,即

yield ['a', 0]
yield ['b', 1]
yield ['c', 2]

each_with_index 另一方面有效:

['a', 'b', 'c'].each_with_index.map(&method(:m))
#=> ["a-0", "b-1", "c-2"]

因为它产生两个独立的值,即元素及其索引,即

yield 'a', 0
yield 'b', 1
yield 'c', 2