3
a_proc = Proc.new {|a,b,*c| p c; c.collect {|i| i*b }}
puts a_proc[2,2,4,3]

根据https://ruby-doc.org/core-2.2.0/Proc.html,上面的代码非常直观, a_proc[2,2,4,3]只是a_proc.call(2,2 ,4,3)隐藏“呼叫”</p>

但是以下(效果很好)让我很困惑

a=[2,2,4,3]
puts a_proc.call(a)
puts a_proc.call(*a)

它似乎与普通的函数调用非常不同,因为它不检查传入的数字参数。

但是,正如预期的那样,如果同样使用参数,方法调用语义将引发错误

def foo(a,b,*c)
  c.collect{|i| i*b}
end
foo([1,2,3,4]) #`block in <main>': wrong number of arguments (given 1, expected 2+) (ArgumentError)

foo(*[1,2,3,4]) #works as expected

我不认为这种不一致是设计故障,因此对此的任何见解都将受到赞赏。

4

2 回答 2

7

块使用与将参数绑定到参数的方法不同的语义。

在这方面,块语义更类似于赋值语义而不是方法语义。事实上,在旧版本的 Ruby 中,块字面上使用赋值来绑定参数,你可以这样写:

class Foo; def bar=(val) puts 'setter called!' end end

some_proc = Proc.new {|$foo, @foo, foo.bar|}
some_proc.call(1, 2, 3)
# setter called!
$foo #=> 1
@foo #=> 2

值得庆幸的是,自从 Ruby 1.9 以来,情况不再如此。但是,保留了一些语义:

  • 如果一个块有多个参数但只接收一个参数,则该参数将被发送一条to_ary消息(如果它还不是Array)并且参数将绑定到Array
  • 如果一个块接收的参数多于它的参数,它会忽略额外的参数
  • 如果一个块接收的参数少于它的参数,那么额外的参数被绑定到nil

注意:#1 是使Hash#each工作如此美妙的原因,否则,您总是必须解构它传递给块的数组。

简而言之,块参数的绑定方式与多重赋值非常相似。您可以想象没有设置器、索引器、全局变量、实例变量和类变量的赋值,只有局部变量,这几乎就是块的参数绑定的工作方式:从块复制&粘贴参数列表,从 复制&粘贴参数列表yield,放中间有一个=标志,你就会明白。

现在,您实际上并不是在谈论块,而是在谈论Proc. 为此,您需要知道一些重要的事情:有两种Procs,不幸的是,它们是使用同一个类实现的。(IMO,它们应该是两个不同的类。)一种称为lambda,另一种通常称为proc(令​​人困惑,因为两者都是Procs)。

Procs 在参数绑定和参数传递(即前面描述的赋值语义)以及return(从最近的词法封闭方法返回)的行为方面都表现得像块。

Lambda 的行为类似于方法,无论是在参数绑定和参数传递(即严格的参数检查)方面,还是在return(它从 lambda 本身返回)的行为方面。

一个简单的助记词:“block”和“proc”押韵,“method”和“lambda”都是希腊语。


对您的问题的一个小评论:

a_proc[2,2,4,3]只是a_proc.call(2,2,4,3)隐藏“调用”的语法糖</p>

不是语法糖。而是Proc简单地定义[]行为与call.

什么语法糖是这样的:

a_proc.(2, 2, 4, 3)

每一次出现

foo.(bar, baz)

被解释为

foo.call(bar, baz)
于 2017-07-29T11:49:11.643 回答
2

我相信可能让您感到困惑的是 Procs 的一些属性。如果给他们一个数组参数,他们会自动 splat。此外,ruby 块通常有一些有趣的方法来处理块参数。您期望的行为是您将使用 Lambda 获得的行为。我建议阅读Proc.lambda?文档在使用数组调用 ruby​​ 块时要小心

现在,让我们从 splat 运算符开始,然后转到 ruby​​ 如何处理块参数:

def foo(a, b, *c) 
  c.map { |i| i * b } # Prefer to use map alias over collect
end

foo([1, 2, 3, 4]) # `block in <main>': wrong number of arguments (given 1, expected 2+) (ArgumentError)

foo(*[1, 2, 3, 4]) # works as expected

因此,在您的论点错误中,这是有道理的:def foo()至少需要两个参数:a, b,以及带有*c. 是splat 运算*符。它会将数组转换为单个参数,或者在相反的情况下,将可变数量的参数转换为数组。因此,当您说 时,您是在给出一个论点,并且它是。您没有设置或。例如,什么会起作用,因为您正在设置,和。这将是同一件事:.foo([1,2,3,4])fooa[1,2,3,4]b*cfoo(1, 1, 1, 2, 3, 4])abcfoo(1, 1, *[1,2,3,4])

现在foo(*[1, 2, 3, 4])可以按预期工作,因为 splat 运算符 ( *) 正在将其转换为foo(1, 2, 3, 4)或等效地foo(1, 2, *[3, 4])

好的,现在我们已经涵盖了 splat 运算符,让我们回顾一下下面的代码(我做了一些小的改动):

a_proc = Proc.new { |a, b, *c| c.map { |i| i * b }}
a = [1, 2, 3, 4]
puts a_proc.call(a)
puts a_proc.call(*a)

请记住,如果块/procs 被赋予单个array参数,它们将自动splat使用它。因此,如果您有一个数组数组arrays = [[1, 1], [2, 2], [3, 3]]并且您这样做了,arrays.each { |a, b| puts "#{a}:#{b}" }那么您将得到1:1, 2:2, 和3:3作为输出。当每个元素作为参数传递给块时,它会看到它是一个数组并对其进行分解,将元素分配给尽可能多的给定块变量。而不是仅仅把那个数组放在a比如 中a = [1, 1]; b = nil,你会得到a = 1; b = 1. 它对 proc 做同样的事情。

a_proc.call([1, 2, 3, 4])变成Proc.new { |1, 2, [3, 4]| c.map { |i| i * b }}并将输出[6, 8]。它会自动拆分它自己的参数。

于 2017-07-29T10:55:08.003 回答