Ruby:Proc#call vs yield

Sam*_*kes 75 ruby yield

thrice方法的Ruby中,以下两个实现之间的行为差​​异是什么?

module WithYield
  def self.thrice
    3.times { yield }      # yield to the implicit block argument
  end
end

module WithProcCall
  def self.thrice(&block)  # & converts implicit block to an explicit, named Proc
    3.times { block.call } # invoke Proc#call
  end
end

WithYield::thrice { puts "Hello world" }
WithProcCall::thrice { puts "Hello world" }
Run Code Online (Sandbox Code Playgroud)

通过"行为差异",我包括错误处理,性能,工具支持等.

jpa*_*zek 51

我认为第一个实际上是另一个的语法糖.换句话说,没有行为差异.

第二种形式允许的是将块"保存"在变量中.然后可以在其他某个时间点调用该块 - 回调.


好.这次我去做了一个快速的基准测试:

require 'benchmark'

class A
  def test
    10.times do
      yield
    end
  end
end

class B
  def test(&block)
    10.times do
      block.call
    end
  end
end

Benchmark.bm do |b|
  b.report do
    a = A.new
    10000.times do
      a.test{ 1 + 1 }
    end
  end

  b.report do
    a = B.new
    10000.times do
      a.test{ 1 + 1 }
    end
  end

  b.report do
    a = A.new
    100000.times do
      a.test{ 1 + 1 }
    end
  end

  b.report do
    a = B.new
    100000.times do
      a.test{ 1 + 1 }
    end
  end

end
Run Code Online (Sandbox Code Playgroud)

结果很有趣:

      user     system      total        real
  0.090000   0.040000   0.130000 (  0.141529)
  0.180000   0.060000   0.240000 (  0.234289)
  0.950000   0.370000   1.320000 (  1.359902)
  1.810000   0.570000   2.380000 (  2.430991)
Run Code Online (Sandbox Code Playgroud)

这表明使用block.call几乎比使用yield慢2 .

  • 我认为如果确实如此,Ruby会更加一致(即如果`yield`只是`Proc#call`的语法糖),但我不认为这是真的.例如,存在不同的错误处理行为(请参阅下面的答案).我也看到它建议(例如http://stackoverflow.com/questions/764134/rubys-yield-feature-in-relation-to-computer-science/765126#765126)`yield`更有效率,因为它不必首先创建一个`Proc`对象,然后调用它的`call`方法. (8认同)
  • 你也错过了第三种情况:`def test(&block); 10.次(和块); 结束`,它应该测试与产量情况相同. (3认同)

Tha*_*you 9

这是Ruby 2.x的更新

ruby 2.0.0p247(2013-06-27修订版41674)[x86_64-darwin12.3.0]

我厌倦了手动编写基准测试,因此我创建了一个名为benchable的小跑步模块

require 'benchable' # https://gist.github.com/naomik/6012505

class YieldCallProc
  include Benchable

  def initialize
    @count = 10000000    
  end

  def bench_yield
    @count.times { yield }
  end

  def bench_call &block
    @count.times { block.call }
  end

  def bench_proc &block
    @count.times &block
  end

end

YieldCallProc.new.benchmark
Run Code Online (Sandbox Code Playgroud)

产量

                      user     system      total        real
bench_yield       0.930000   0.000000   0.930000 (  0.928682)
bench_call        1.650000   0.000000   1.650000 (  1.652934)
bench_proc        0.570000   0.010000   0.580000 (  0.578605)
Run Code Online (Sandbox Code Playgroud)

我认为这里最令人惊讶的bench_yield是比它慢bench_proc.我希望我能更好地理解为什么会这样.

  • 我相信这是因为在`bench_proc`中,一元运算符实际上将proc转换为`times`调用的块,跳过了`bench_yield`和`bench_call`中`times`的块创建开销.这是一种奇怪的特殊情况用途,看起来`yield`在大多数情况下仍然更快.有关proc to block assignment的更多信息:http://ablogaboutcode.com/2012/01/04/the-ampersand-operator-in-ruby/(section:The Unary&) (2认同)

Sam*_*kes 6

如果您忘记传递块,它们会给出不同的错误消息:

> WithYield::thrice
LocalJumpError: no block given
        from (irb):3:in `thrice'
        from (irb):3:in `times'
        from (irb):3:in `thrice'

> WithProcCall::thrice
NoMethodError: undefined method `call' for nil:NilClass
        from (irb):9:in `thrice'
        from (irb):9:in `times'
        from (irb):9:in `thrice'
Run Code Online (Sandbox Code Playgroud)

但如果您尝试传递"正常"(非阻塞)参数,它们的行为相同:

> WithYield::thrice(42)
ArgumentError: wrong number of arguments (1 for 0)
        from (irb):19:in `thrice'

> WithProcCall::thrice(42)
ArgumentError: wrong number of arguments (1 for 0)
        from (irb):20:in `thrice'
Run Code Online (Sandbox Code Playgroud)


cbr*_*hli 6

其他答案非常详尽,Ruby中的Closures广泛涵盖了功能差异.我很好奇哪种方法对于可选择接受块的方法最有效,所以我写了一些基准(关于这个Paul Mucur的帖子).我比较了三种方法:

  • &阻止方法签名
  • 运用 &Proc.new
  • 包裹yield在另一个街区

这是代码:

require "benchmark"

def always_yield
  yield
end

def sometimes_block(flag, &block)
  if flag && block
    always_yield &block
  end
end

def sometimes_proc_new(flag)
  if flag && block_given?
    always_yield &Proc.new
  end
end

def sometimes_yield(flag)
  if flag && block_given?
    always_yield { yield }
  end
end

a = b = c = 0
n = 1_000_000
Benchmark.bmbm do |x|
  x.report("no &block") do
    n.times do
      sometimes_block(false) { "won't get used" }
    end
  end
  x.report("no Proc.new") do
    n.times do
      sometimes_proc_new(false) { "won't get used" }
    end
  end
  x.report("no yield") do
    n.times do
      sometimes_yield(false) { "won't get used" }
    end
  end

  x.report("&block") do
    n.times do
      sometimes_block(true) { a += 1 }
    end
  end
  x.report("Proc.new") do
    n.times do
      sometimes_proc_new(true) { b += 1 }
    end
  end
  x.report("yield") do
    n.times do
      sometimes_yield(true) { c += 1 }
    end
  end
end
Run Code Online (Sandbox Code Playgroud)

Ruby 2.0.0p247和1.9.3p392之间的性能相似.以下是1.9.3的结果:

                  user     system      total        real
no &block     0.580000   0.030000   0.610000 (  0.609523)
no Proc.new   0.080000   0.000000   0.080000 (  0.076817)
no yield      0.070000   0.000000   0.070000 (  0.077191)
&block        0.660000   0.030000   0.690000 (  0.689446)
Proc.new      0.820000   0.030000   0.850000 (  0.849887)
yield         0.250000   0.000000   0.250000 (  0.249116)
Run Code Online (Sandbox Code Playgroud)

&block不总是使用时添加显式参数确实会减慢方法的速度.如果块是可选的,请不要将其添加到方法签名中.并且,为了传递块,yield在另一个块中包装是最快的.

也就是说,这些是一百万次迭代的结果,所以不要太担心它.如果一种方法以百万分之一秒的代价使您的代码更清晰,那么无论如何都要使用它.