Reputation: 40731
Let's go to the code directly:
#!/usr/bin/ruby
require 'tk'
class Epg
def initialize
@var = "bad"
@cvs = nil
@items_demo = TkRoot.new() {title "EPG"}
TkFrame.new(@items_demo) {|cf|
@var = "good"
@cvs = TkCanvas.new(cf) {|c|}
puts "@cvs 1 is #{@cvs}"
puts "@var 1 is #{@var}"
}.pack('side'=>'top', 'fill'=>'both', 'expand'=>'yes')
puts "@cvs 2 is #{@cvs}"
puts "@var 2 is #{@var}"
end #initialize
def test
@var = "bad"
puts " @var 3 :#{@var}"
(1..3).each {|x| @var="good"}
puts " @var 4 :#{@var}"
end
end
e= Epg.new
e.test
Here is the output:
@cvs 1 is #<Tk::Canvas:0xb7cecb08>
@var 1 is good
@cvs 2 is
@var 2 is bad #@var has NOT been changed by the code in the block
@var 3 :bad
@var 4 :good #@var has been changed by the code in the block
Why we see different behavior here?
Upvotes: 3
Views: 748
Reputation: 28703
You can think of blocks as closing over both the set of local variables and the current self
.
In Ruby, you will always have access to local variables, no matter what. The self
encapsulates instance methods on the current object as well as instance variables.
Consider the following code:
class Table
def initialize(legs)
@legs = legs
end
def with_legs
yield @legs
end
end
And then:
def some_calling_method
name = "Yehuda"
Table.new(4) {|legs| puts "#{name} gnaws off one of the #{legs} legs" }
end
By Ruby's block semantics, you can be assured that name
will be available inside the block, even without looking at the method you're calling.
However, consider the following:
class Person
def initialize(name)
@name = name
end
def gnaw
Table.new(4).with_legs do |legs|
puts "#{@name} gnaws off one of the #{legs} legs"
end
end
end
Person.new("Yehuda").gnaw
In this case, we are accessing the @name
instance variable from inside the block. It works great in this case, but is not guaranteed. What if we implemented table a bit differently:
class Table
def initialize(legs)
@legs = legs
end
def with_legs(&block)
self.instance_eval(&block)
end
end
Effectively, what we're saying is "evaluate the block in the context of a different self." In this case, we are evaluating the block in the context of the table. Why would you do that?
class Leg
attr_accessor :number
def initialize(number)
@number = number
end
end
class Table
def initialize(legs)
@legs = legs
end
def with_leg(&block)
Leg.new(rand(@legs).instance_eval(&block)
end
end
Now, you could do:
class Person
def initialize(name)
@name = name
end
def gnaw
Table.new(4).with_leg do
puts "I'm gnawing off one of leg #{number}"
end
end
end
If you wanted access to the person object inside of the block, you'd have to do:
class Person
def initialize(name)
@name = name
end
def gnaw
my_name = name
Table.new(4).with_leg do
puts "#{my_name} is gnawing off one of leg #{number}"
end
end
end
As you can see, the use of instance_eval can make it simpler and less bulky to access methods of a far-off object inside a block, but comes at the cost of making the self
unaccessible. The technique is usually used in DSLs, where a number of methods are injected into the block, but the self doesn't matter that much.
This is what's happening with Tk; they're using instance_eval to inject their own self
into the block, which is wiping your self
clean.
Upvotes: 5
Reputation: 2179
The explanation is that TkFrame.new uses instance_eval and thus the assignment @var = "good" changes the instance variable of TkFrame. Try this out:
class A def initialize(&b) instance_eval(&b) end end class B def initialize @x = 10 @a = A.new do @x = 20 end end end p B.new
This is what you'll see:
#<B:0x10141314 @x=10, @a=#<A:0x10141300 @x=20>>
Upvotes: 4