How are variables bound to the body of a define_me

2019-05-01 16:21发布

While trying to brush up my Ruby skills I keep running across this case which I can't figure out an explanation for by just reading the API docs. An explanation would be greatly appreciated. Here's the example code:

for name in [ :new, :create, :destroy ]
  define_method("test_#{name}") do
    puts name
  end
end

What I want/expect to happen is that the name variable will be bound to the block given to define_method and that when #test_new is called it will output "new". Instead each defined method outputs "destroy" -- the last value assigned to the name variable. What am I misunderstanding about define_method and its blocks? Thanks!

3条回答
闹够了就滚
2楼-- · 2019-05-01 16:55

Blocks in Ruby are closures: the block you pass to define_method captures the variable name itself–not its value—so that it remains in scope whenever that block is called. That's the first piece of the puzzle.

The second piece is that the method defined by define_method is the block itself. Basically, it converts a Proc object (the block passed to it) into a Method object, and binds it to the receiver.

So what you end up with is a method that has captured (is closed over) the variable name, which by the time your loop completes is set to :destroy.

Addition: The for ... in construction actually creates a new local variable, which the corresponding [ ... ].each {|name| ... } construction would not do. That is, your for ... in loop is equivalent to the following (in Ruby 1.8 anyway):

name = nil
[ :new, :create, :destroy ].each do |name|
  define_method("test_#{name}") do
    puts name
  end
end
name # => :destroy
查看更多
相关推荐>>
3楼-- · 2019-05-01 16:56
for name in [ :new, :create, :destroy ]
  local_name = name
  define_method("test_#{local_name}") do
    puts local_name
  end
end

This method will behave as you expect. The reason for the confusion is that 'name' is not created once per iteration of the for loop. It is created once, and incremented. In addition, if I understand correctly, method definitions are not closures like other blocks. They retain variable visibility, but do not close over the current value of the variables.

查看更多
Rolldiameter
4楼-- · 2019-05-01 16:59

The problem here is that for loop expressions do not create a new scope. The only things that create new scopes in Ruby are script bodies, module bodies, class bodies, method bodies and blocks.

If you actually look up the behavior of for loop expressions in the Draft ISO Ruby Specification, you will find that a for loop expression gets executed exactly like an each iterator except for the fact that it does not create a new scope.

No Rubyist would ever use a for loop, anyway: they would use an iterator instead, which does take a block and thus creates a new scope.

If you use an idiomatic iterator, everything works as expected:

class Object
  %w[new create destroy].each do |name|
    define_method "test_#{name}" do
      puts name
    end
  end
end

require 'test/unit'
require 'stringio'
class TestDynamicMethods < Test::Unit::TestCase
  def setup; @old_stdout, $> = $>, (@fake_logdest = StringIO.new) end
  def teardown; $> = @old_stdout end

  def test_that_the_test_create_method_prints_create
    Object.new.test_create
    assert_equal "create\n", @fake_logdest.string
  end
  def test_that_the_test_destroy_method_prints_destroy
    Object.new.test_destroy
    assert_equal "destroy\n", @fake_logdest.string
  end
  def test_that_the_test_new_method_prints_new
    Object.new.test_new
    assert_equal "new\n", @fake_logdest.string
  end
end
查看更多
登录 后发表回答