class Foo
attr_accessor :x
def initialize(x)
@x = x
end
def count_down
x -= 1
end
end
If I then do
f = Foo.new(3)
f.count_down
I thought that I would decrement the x and then return 2, however I
get NoMethodError: undefined method '-' for nil:NilClass instead.
I thought that the attr_accessor just gave me a x and x= method,
which read and wrote to @x.
Even if I switch the count_down definition to:
def count_count
x = x - 1
end
it fails with the same NoMethodError.
The implementation works, if you substitute the first x in count_down with either self.x or @x. However, as I stated above, I thought that attr_accessor gave me the following methods:
def x
@x
end
def x=(other)
@x = other
end
and the line in count_down should in my opinion call x= with the x-1, where the x is calling the x method, retrieving @x and subtracting 1.
Shouldn’t Ruby look for all possible methods and variables, before declaring that the x = means “assign a new variable”?
… the funny part is also the error message, which states that NoMethodError: undefined method '-' for nil:NilClass, which seems to indicate that it is the second x, which is read as nil, instead of the err being with the assigning part.
class Foo
def initialize(x)
@x = x
end
def inspect_x
puts defined?(x)
puts defined?(self.x)
puts defined?(@x)
end
def x
puts :reader
@x
end
def x=(y)
puts :writer
@x = y
end
def cdx
x -= 1
end
def cdselfx
self.x -= 1
end
def cdatx
@x -= 1
end
end
x = Foo.new(3)
# => #<Foo:0x00000001381b60 @x=3>
x.inspect_x
#method
#method
#instance-variable
x.cdx
#NoMethodError: undefined method `-' for nil:NilClass
x.cdselfx
#reader
#writer
# => 2
x.cdselfx
#reader
#writer
# => 1
x = Foo.new(3)
# => #<Foo:0x000000012fc438 @x=3>
x.cdatx
# => 2
x.cdatx
# => 1
For some reason since you’re using assignment on an instance variable the instance reference is required regardless of whether you use the method or instance variable.
If you add this method you can see the writer method isn’t getting called. x becomes a local (scope) variable, rather than the instance variable or method, when assigned.
class Foo
def count_down
y = x
x = y-1
end
end
x = Foo.new(3)
x.count_down
#reader
# => 2
x.count_down
#reader
# => 2
With some inspection
class Foo
def count_down
y = x
x = y-1
puts defined?(x)
x
end
end
x = Foo.new(3)
x.count_down
#reader
#local-variable
# => 2
x.count_down
#reader
#local-variable
# => 2
So to answer your question the -= operand takes x (local variable nil) to perform itself on. I just learned this myself. I don’t use attr methods… maybe ever.
I think you’re right. I just got caught in this simple trap writing some code yesterday. If you have a large class with many attr_reader, attr_writer and attr_accessor, it might get overwhelming to check all the instance variables before creating local variables.
Accessor are here for outside access. Use self.x in this case (self access your object from outside, so you get access only to public methods). Without accessor, use @x to access your Instance Variable. It’s the normal way in Ruby. You define a @variable, you use @variable in your code.
In your case (x -= 1), internally, Ruby first get the x object (here x don’t exist, so it’s initialized to nil), and attempt to call on x the method “-” with your parameter (here 1). The fact that Ruby decompose this simple line in two actions can be checked here :
In irb, type :
x # You get undefined local variable or method x
x += 1 # You get the undefined method + for nil class
x # You get nil. Ruby in it's first action has created your x variable to nil
Great, no ?
This is why you get this strange error about a missing method : Numeric objects have the “-” method (and lot of other, but it’s not our problem here). Nill object have not. So you get a method error. Really, it’s normal.
In irb, try to do :
1.respond_to? :-
Prefer use @x in your code and let the use of your accessors for outside access only.
Always use self when you want use setter following assignment signature, in short : Any method name ending with “=”… And especially if you use object with dynamic method generation like ActiveRecord does (at least, one method ending with “=” for each field).
class Foo
attr_accessor :x
def initialize(x)
@x = x
end
def count_down
send :x=, x()-1
end
end
a = Foo.new(3)
puts a.count_down
# => 2
puts a.count_down
# => 1