irb中的块和调用范围行为不一致

时间:2016-09-29 22:42:03

标签: ruby-on-rails ruby stack-trace irb

我一直在尝试使用Spring预加载器在Rails 4.2.7中使用一些内部rails代码,并在irb中发现堆栈转储的一些奇怪行为。

考虑以下代码

def run_block
  yield
end

class Vehicle < ActiveRecord::Base
  def self.instantiate(*args)
    puts caller_locations
    super
  end

  def self.test_stack
    run_block {Vehicle.all}
  end
end

注意我正在扩展实例化函数(http://apidock.com/rails/ActiveRecord/Base/instantiate/class)的功能以打印出调用堆栈。

当我打开rails c控制台并运行

run_block {puts caller_locations}

我得到了堆栈跟踪

test_irb/app/models/vehicle.rb:2:in `run_block'
(irb):4:in `irb_binding'
.rvm/rubies/ruby-head/lib/ruby/2.4.0/irb/workspace.rb:87:in `eval'
.rvm/rubies/ruby-head/lib/ruby/2.4.0/irb/workspace.rb:87:in `evaluate'
.rvm/rubies/ruby-head/lib/ruby/2.4.0/irb/context.rb:380:in `evaluate'
.rvm/rubies/ruby-head/lib/ruby/2.4.0/irb.rb:489:in `block (2 levels) in eval_input'
.rvm/rubies/ruby-head/lib/ruby/2.4.0/irb.rb:623:in `signal_status'
.rvm/rubies/ruby-head/lib/ruby/2.4.0/irb.rb:486:in `block in eval_input'
.rvm/rubies/ruby-head/lib/ruby/2.4.0/irb/ruby-lex.rb:246:in `block (2 levels) in each_top_level_statement'
.rvm/rubies/ruby-head/lib/ruby/2.4.0/irb/ruby-lex.rb:232:in `loop'
.rvm/rubies/ruby-head/lib/ruby/2.4.0/irb/ruby-lex.rb:232:in `block in each_top_level_statement'
.rvm/rubies/ruby-head/lib/ruby/2.4.0/irb/ruby-lex.rb:231:in `catch'
.rvm/rubies/ruby-head/lib/ruby/2.4.0/irb/ruby-lex.rb:231:in `each_top_level_statement'
.rvm/rubies/ruby-head/lib/ruby/2.4.0/irb.rb:485:in `eval_input'
.rvm/rubies/ruby-head/lib/ruby/2.4.0/irb.rb:395:in `block in start'
.rvm/rubies/ruby-head/lib/ruby/2.4.0/irb.rb:394:in `catch'
.rvm/rubies/ruby-head/lib/ruby/2.4.0/irb.rb:394:in `start'
...

但是运行(假设我在数据库中有一些行)

run_block {Vehicle.all}

我得到了堆栈跟踪

.rvm/gems/ruby-head/gems/activerecord-4.2.7/lib/active_record/querying.rb:50:in `block (2 levels) in find_by_sql'
.rvm/gems/ruby-head/gems/activerecord-4.2.7/lib/active_record/result.rb:51:in `block in each'
.rvm/gems/ruby-head/gems/activerecord-4.2.7/lib/active_record/result.rb:51:in `each'
.rvm/gems/ruby-head/gems/activerecord-4.2.7/lib/active_record/result.rb:51:in `each'
.rvm/gems/ruby-head/gems/activerecord-4.2.7/lib/active_record/querying.rb:50:in `map'
.rvm/gems/ruby-head/gems/activerecord-4.2.7/lib/active_record/querying.rb:50:in `block in find_by_sql'
.rvm/gems/ruby-head/gems/activesupport-4.2.7/lib/active_support/notifications/instrumenter.rb:20:in `instrument'
.rvm/gems/ruby-head/gems/activerecord-4.2.7/lib/active_record/querying.rb:49:in `find_by_sql'
.rvm/gems/ruby-head/gems/activerecord-4.2.7/lib/active_record/relation.rb:639:in `exec_queries'
.rvm/gems/ruby-head/gems/activerecord-4.2.7/lib/active_record/relation.rb:515:in `load'
.rvm/gems/ruby-head/gems/activerecord-4.2.7/lib/active_record/relation.rb:243:in `to_a'
.rvm/gems/ruby-head/gems/activerecord-4.2.7/lib/active_record/relation.rb:630:in `inspect'
.rvm/rubies/ruby-head/lib/ruby/2.4.0/irb/inspector.rb:109:in `block in <module:IRB>'
.rvm/rubies/ruby-head/lib/ruby/2.4.0/irb/inspector.rb:102:in `inspect_value'
.rvm/rubies/ruby-head/lib/ruby/2.4.0/irb/context.rb:384:in `inspect_last_value'
.rvm/rubies/ruby-head/lib/ruby/2.4.0/irb.rb:661:in `output_value'
.rvm/rubies/ruby-head/lib/ruby/2.4.0/irb.rb:490:in `block (2 levels) in eval_input'
.rvm/rubies/ruby-head/lib/ruby/2.4.0/irb.rb:623:in `signal_status'

没有显示该块嵌套在跟踪中任何位置的run_block方法中。但是,运行run_block {puts Vehicle.all}会在堆栈跟踪中显示run_block函数。同样,在块中运行Vehicle.first会显示预期的堆栈跟踪。但上面定义的Vehicle.test_stack在堆栈跟踪中既未显示run_block方法也未显示test_stack方法。任何人都可以解释这种不一致吗?这是ruby语言结构的结果还是实现.all方法的结果?谢谢大家!

1 个答案:

答案 0 :(得分:1)

This is a tricky one. The instantiate stack trace doesn't show run_block because instantiate isn't called from within run_block.

Here's the deal: when you call Vehicle.all, it doesn't return an array of Vehicle objects, each of which has been instantiated; it just returns an ActiveRecord::Relation object that represents the query yet to be run. Since that's the last expression in the block passed to run_block, it's the return value of the block, and since yield is the last statement in run_block, the ActiveRecord::Relation object is also the return value of that method.

However, when you call a method from IRB, it prints what was returned, in the form of the the results of calling Object#inspect on it, and when you call inspect on an ActiveRecord::Relation object, it tries to include in the output the strings that represent each object in the relation. In order to do that, it has to actually run the query and instantiate the Model objects, so it does, and your instantiate override is finally called. But that's after the run_block block has already exited!

Indeed, if you ran (in IRB) run_block {Vehicle.all} ; nil on a single line, IRB would print nil as the result of evaluating that, and your instantiate method would never be called at all.

When you use run_block { puts Vehicle.all }, then the Relation object has be actual realized for the puts, which takes place inside the block so in that case you do see run_block in the stack trace.